Complete batch 012 (golden set diff) and 013 (advisory chat), fix build errors

Sprints completed:
- SPRINT_20260110_012_* (golden set diff layer - 10 sprints)
- SPRINT_20260110_013_* (advisory chat - 4 sprints)

Build fixes applied:
- Fix namespace conflicts with Microsoft.Extensions.Options.Options.Create
- Fix VexDecisionReachabilityIntegrationTests API drift (major rewrite)
- Fix VexSchemaValidationTests FluentAssertions method name
- Fix FixChainGateIntegrationTests ambiguous type references
- Fix AdvisoryAI test files required properties and namespace aliases
- Add stub types for CveMappingController (ICveSymbolMappingService)
- Fix VerdictBuilderService static context issue

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
master
2026-01-11 10:09:07 +02:00
parent a3b2f30a11
commit 7f7eb8b228
232 changed files with 58979 additions and 91 deletions

View File

@@ -0,0 +1,346 @@
using System.Collections.Immutable;
namespace StellaOps.BinaryIndex.GoldenSet;
/// <summary>
/// Storage interface for golden set definitions.
/// </summary>
public interface IGoldenSetStore
{
/// <summary>
/// Stores a golden set definition.
/// </summary>
/// <param name="definition">The definition to store.</param>
/// <param name="status">Initial status (default: Draft).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Store result with content digest.</returns>
Task<GoldenSetStoreResult> StoreAsync(
GoldenSetDefinition definition,
GoldenSetStatus status = GoldenSetStatus.Draft,
CancellationToken ct = default);
/// <summary>
/// Retrieves a golden set by ID.
/// </summary>
/// <param name="goldenSetId">The golden set ID (CVE/GHSA ID).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The definition or null if not found.</returns>
Task<GoldenSetDefinition?> GetByIdAsync(
string goldenSetId,
CancellationToken ct = default);
/// <summary>
/// Retrieves a golden set by content digest.
/// </summary>
/// <param name="contentDigest">The content-addressed digest.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The definition or null if not found.</returns>
Task<GoldenSetDefinition?> GetByDigestAsync(
string contentDigest,
CancellationToken ct = default);
/// <summary>
/// Lists golden sets matching criteria.
/// </summary>
/// <param name="query">Query parameters.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of matching golden set summaries.</returns>
Task<ImmutableArray<GoldenSetSummary>> ListAsync(
GoldenSetListQuery query,
CancellationToken ct = default);
/// <summary>
/// Updates the status of a golden set.
/// </summary>
/// <param name="goldenSetId">The golden set ID.</param>
/// <param name="status">New status.</param>
/// <param name="reviewedBy">Reviewer ID (for InReview->Approved).</param>
/// <param name="ct">Cancellation token.</param>
Task UpdateStatusAsync(
string goldenSetId,
GoldenSetStatus status,
string? reviewedBy = null,
CancellationToken ct = default);
/// <summary>
/// Updates the status of a golden set with a comment.
/// </summary>
/// <param name="goldenSetId">The golden set ID.</param>
/// <param name="status">New status.</param>
/// <param name="actorId">Who made the change.</param>
/// <param name="comment">Comment explaining the change.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Update result.</returns>
Task<GoldenSetStoreResult> UpdateStatusAsync(
string goldenSetId,
GoldenSetStatus status,
string actorId,
string comment,
CancellationToken ct = default);
/// <summary>
/// Retrieves a golden set with its current status.
/// </summary>
/// <param name="goldenSetId">The golden set ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The stored golden set or null if not found.</returns>
Task<StoredGoldenSet?> GetAsync(
string goldenSetId,
CancellationToken ct = default);
/// <summary>
/// Gets the audit log for a golden set.
/// </summary>
/// <param name="goldenSetId">The golden set ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Audit log entries ordered by timestamp descending.</returns>
Task<ImmutableArray<GoldenSetAuditEntry>> GetAuditLogAsync(
string goldenSetId,
CancellationToken ct = default);
/// <summary>
/// Gets all golden sets applicable to a component.
/// </summary>
/// <param name="component">Component name.</param>
/// <param name="statusFilter">Optional status filter (default: Approved).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of applicable golden sets.</returns>
Task<ImmutableArray<GoldenSetDefinition>> GetByComponentAsync(
string component,
GoldenSetStatus? statusFilter = GoldenSetStatus.Approved,
CancellationToken ct = default);
/// <summary>
/// Deletes a golden set (soft delete - moves to Archived).
/// </summary>
/// <param name="goldenSetId">The golden set ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>True if deleted; false if not found.</returns>
Task<bool> DeleteAsync(
string goldenSetId,
CancellationToken ct = default);
}
/// <summary>
/// Result of storing a golden set.
/// </summary>
public sealed record GoldenSetStoreResult
{
/// <summary>
/// Whether the operation succeeded.
/// </summary>
public required bool Success { get; init; }
/// <summary>
/// Content digest of the stored definition.
/// </summary>
public required string ContentDigest { get; init; }
/// <summary>
/// Whether an existing record was updated.
/// </summary>
public bool WasUpdated { get; init; }
/// <summary>
/// Error message if operation failed.
/// </summary>
public string? Error { get; init; }
/// <summary>
/// Creates a success result.
/// </summary>
public static GoldenSetStoreResult Succeeded(string contentDigest, bool wasUpdated = false) => new()
{
Success = true,
ContentDigest = contentDigest,
WasUpdated = wasUpdated
};
/// <summary>
/// Creates a failure result.
/// </summary>
public static GoldenSetStoreResult Failed(string error) => new()
{
Success = false,
ContentDigest = string.Empty,
Error = error
};
}
/// <summary>
/// Summary of a golden set for listing.
/// </summary>
public sealed record GoldenSetSummary
{
/// <summary>
/// Golden set ID (CVE/GHSA ID).
/// </summary>
public required string Id { get; init; }
/// <summary>
/// Component name.
/// </summary>
public required string Component { get; init; }
/// <summary>
/// Current status.
/// </summary>
public required GoldenSetStatus Status { get; init; }
/// <summary>
/// Number of vulnerable targets.
/// </summary>
public required int TargetCount { get; init; }
/// <summary>
/// Creation timestamp.
/// </summary>
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Review timestamp (if reviewed).
/// </summary>
public DateTimeOffset? ReviewedAt { get; init; }
/// <summary>
/// Content digest.
/// </summary>
public required string ContentDigest { get; init; }
/// <summary>
/// Tags for filtering.
/// </summary>
public ImmutableArray<string> Tags { get; init; } = [];
}
/// <summary>
/// Query parameters for listing golden sets.
/// </summary>
public sealed record GoldenSetListQuery
{
/// <summary>
/// Filter by component name.
/// </summary>
public string? ComponentFilter { get; init; }
/// <summary>
/// Filter by status.
/// </summary>
public GoldenSetStatus? StatusFilter { get; init; }
/// <summary>
/// Filter by tags (any match).
/// </summary>
public ImmutableArray<string>? TagsFilter { get; init; }
/// <summary>
/// Filter by creation date (after).
/// </summary>
public DateTimeOffset? CreatedAfter { get; init; }
/// <summary>
/// Filter by creation date (before).
/// </summary>
public DateTimeOffset? CreatedBefore { get; init; }
/// <summary>
/// Maximum results to return.
/// </summary>
public int Limit { get; init; } = 100;
/// <summary>
/// Offset for pagination.
/// </summary>
public int Offset { get; init; } = 0;
/// <summary>
/// Order by field.
/// </summary>
public GoldenSetOrderBy OrderBy { get; init; } = GoldenSetOrderBy.CreatedAtDesc;
}
/// <summary>
/// Ordering options for golden set listing.
/// </summary>
public enum GoldenSetOrderBy
{
/// <summary>Order by ID ascending.</summary>
IdAsc,
/// <summary>Order by ID descending.</summary>
IdDesc,
/// <summary>Order by creation date ascending.</summary>
CreatedAtAsc,
/// <summary>Order by creation date descending.</summary>
CreatedAtDesc,
/// <summary>Order by component ascending.</summary>
ComponentAsc,
/// <summary>Order by component descending.</summary>
ComponentDesc
}
/// <summary>
/// A stored golden set with its current status.
/// </summary>
public sealed record StoredGoldenSet
{
/// <summary>
/// The golden set definition.
/// </summary>
public required GoldenSetDefinition Definition { get; init; }
/// <summary>
/// Current status.
/// </summary>
public required GoldenSetStatus Status { get; init; }
/// <summary>
/// When the record was created.
/// </summary>
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// When the record was last updated.
/// </summary>
public required DateTimeOffset UpdatedAt { get; init; }
}
/// <summary>
/// An entry in the golden set audit log.
/// </summary>
public sealed record GoldenSetAuditEntry
{
/// <summary>
/// Operation performed.
/// </summary>
public required string Operation { get; init; }
/// <summary>
/// Who performed the operation.
/// </summary>
public required string ActorId { get; init; }
/// <summary>
/// When the operation occurred.
/// </summary>
public required DateTimeOffset Timestamp { get; init; }
/// <summary>
/// Status before the operation.
/// </summary>
public GoldenSetStatus? OldStatus { get; init; }
/// <summary>
/// Status after the operation.
/// </summary>
public GoldenSetStatus? NewStatus { get; init; }
/// <summary>
/// Comment associated with the operation.
/// </summary>
public string? Comment { get; init; }
}

View File

@@ -0,0 +1,665 @@
using System.Collections.Immutable;
using System.Globalization;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Npgsql;
namespace StellaOps.BinaryIndex.GoldenSet;
/// <summary>
/// PostgreSQL implementation of <see cref="IGoldenSetStore"/>.
/// </summary>
internal sealed class PostgresGoldenSetStore : IGoldenSetStore
{
private readonly NpgsqlDataSource _dataSource;
private readonly IGoldenSetValidator _validator;
private readonly TimeProvider _timeProvider;
private readonly GoldenSetOptions _options;
private readonly ILogger<PostgresGoldenSetStore> _logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = false
};
/// <summary>
/// Initializes a new instance of <see cref="PostgresGoldenSetStore"/>.
/// </summary>
public PostgresGoldenSetStore(
NpgsqlDataSource dataSource,
IGoldenSetValidator validator,
TimeProvider timeProvider,
IOptions<GoldenSetOptions> options,
ILogger<PostgresGoldenSetStore> logger)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
_validator = validator ?? throw new ArgumentNullException(nameof(validator));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<GoldenSetStoreResult> StoreAsync(
GoldenSetDefinition definition,
GoldenSetStatus status = GoldenSetStatus.Draft,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(definition);
// Validate first
var validation = await _validator.ValidateAsync(definition, ct: ct);
if (!validation.IsValid)
{
var errorMessage = string.Join("; ", validation.Errors.Select(e => e.Message));
_logger.LogWarning("Validation failed for golden set {Id}: {Errors}", definition.Id, errorMessage);
return GoldenSetStoreResult.Failed(errorMessage);
}
var digest = validation.ContentDigest!;
var yaml = GoldenSetYamlSerializer.Serialize(definition);
var json = JsonSerializer.Serialize(definition, JsonOptions);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var tx = await conn.BeginTransactionAsync(ct);
try
{
var wasUpdated = await UpsertDefinitionAsync(conn, definition, status, yaml, json, digest, ct);
await DeleteTargetsAsync(conn, definition.Id, ct);
await InsertTargetsAsync(conn, definition, ct);
await InsertAuditLogAsync(conn, definition.Id, wasUpdated ? "updated" : "created",
definition.Metadata.AuthorId, null, status.ToString(), null, ct);
await tx.CommitAsync(ct);
_logger.LogInformation("Stored golden set {Id} with digest {Digest} (updated={Updated})",
definition.Id, digest, wasUpdated);
return GoldenSetStoreResult.Succeeded(digest, wasUpdated);
}
catch (Exception ex)
{
await tx.RollbackAsync(ct);
_logger.LogError(ex, "Failed to store golden set {Id}", definition.Id);
throw;
}
}
/// <inheritdoc />
public async Task<GoldenSetDefinition?> GetByIdAsync(string goldenSetId, CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(goldenSetId);
const string sql = """
SELECT definition_yaml
FROM golden_sets.definitions
WHERE id = @id
""";
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", goldenSetId);
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (!await reader.ReadAsync(ct))
{
return null;
}
var yaml = reader.GetString(0);
return GoldenSetYamlSerializer.Deserialize(yaml);
}
/// <inheritdoc />
public async Task<GoldenSetDefinition?> GetByDigestAsync(string contentDigest, CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(contentDigest);
const string sql = """
SELECT definition_yaml
FROM golden_sets.definitions
WHERE content_digest = @digest
""";
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@digest", contentDigest);
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (!await reader.ReadAsync(ct))
{
return null;
}
var yaml = reader.GetString(0);
return GoldenSetYamlSerializer.Deserialize(yaml);
}
/// <inheritdoc />
public async Task<ImmutableArray<GoldenSetSummary>> ListAsync(GoldenSetListQuery query, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(query);
var conditions = new List<string>();
var parameters = new Dictionary<string, object>();
if (!string.IsNullOrWhiteSpace(query.ComponentFilter))
{
conditions.Add("component = @component");
parameters["@component"] = query.ComponentFilter;
}
if (query.StatusFilter.HasValue)
{
conditions.Add("status = @status");
parameters["@status"] = query.StatusFilter.Value.ToString().ToLowerInvariant();
}
if (query.TagsFilter.HasValue && !query.TagsFilter.Value.IsEmpty)
{
conditions.Add("tags && @tags");
parameters["@tags"] = query.TagsFilter.Value.ToArray();
}
if (query.CreatedAfter.HasValue)
{
conditions.Add("created_at >= @created_after");
parameters["@created_after"] = query.CreatedAfter.Value;
}
if (query.CreatedBefore.HasValue)
{
conditions.Add("created_at <= @created_before");
parameters["@created_before"] = query.CreatedBefore.Value;
}
var whereClause = conditions.Count > 0 ? "WHERE " + string.Join(" AND ", conditions) : "";
var orderClause = query.OrderBy switch
{
GoldenSetOrderBy.IdAsc => "ORDER BY id ASC",
GoldenSetOrderBy.IdDesc => "ORDER BY id DESC",
GoldenSetOrderBy.CreatedAtAsc => "ORDER BY created_at ASC",
GoldenSetOrderBy.CreatedAtDesc => "ORDER BY created_at DESC",
GoldenSetOrderBy.ComponentAsc => "ORDER BY component ASC",
GoldenSetOrderBy.ComponentDesc => "ORDER BY component DESC",
_ => "ORDER BY created_at DESC"
};
var sql = string.Format(
CultureInfo.InvariantCulture,
"""
SELECT id, component, status, target_count, created_at, reviewed_at, content_digest, tags
FROM golden_sets.definitions
{0}
{1}
LIMIT @limit OFFSET @offset
""",
whereClause,
orderClause);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = new NpgsqlCommand(sql, conn);
foreach (var (key, value) in parameters)
{
cmd.Parameters.AddWithValue(key, value);
}
cmd.Parameters.AddWithValue("@limit", query.Limit);
cmd.Parameters.AddWithValue("@offset", query.Offset);
var results = ImmutableArray.CreateBuilder<GoldenSetSummary>();
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
results.Add(new GoldenSetSummary
{
Id = reader.GetString(0),
Component = reader.GetString(1),
Status = Enum.Parse<GoldenSetStatus>(reader.GetString(2), ignoreCase: true),
TargetCount = reader.GetInt32(3),
CreatedAt = reader.GetFieldValue<DateTimeOffset>(4),
ReviewedAt = reader.IsDBNull(5) ? null : reader.GetFieldValue<DateTimeOffset>(5),
ContentDigest = reader.GetString(6),
Tags = reader.IsDBNull(7) ? [] : ((string[])reader.GetValue(7)).ToImmutableArray()
});
}
return results.ToImmutable();
}
/// <inheritdoc />
public async Task UpdateStatusAsync(
string goldenSetId,
GoldenSetStatus status,
string? reviewedBy = null,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(goldenSetId);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var tx = await conn.BeginTransactionAsync(ct);
try
{
// Get current status
var currentStatus = await GetCurrentStatusAsync(conn, goldenSetId, ct);
if (currentStatus is null)
{
throw new InvalidOperationException($"Golden set {goldenSetId} not found");
}
// Update status
var sql = status is GoldenSetStatus.Approved or GoldenSetStatus.InReview
? """
UPDATE golden_sets.definitions
SET status = @status, reviewed_by = @reviewed_by, reviewed_at = @reviewed_at
WHERE id = @id
"""
: """
UPDATE golden_sets.definitions
SET status = @status
WHERE id = @id
""";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", goldenSetId);
cmd.Parameters.AddWithValue("@status", status.ToString().ToLowerInvariant());
if (status is GoldenSetStatus.Approved or GoldenSetStatus.InReview)
{
cmd.Parameters.AddWithValue("@reviewed_by", (object?)reviewedBy ?? DBNull.Value);
cmd.Parameters.AddWithValue("@reviewed_at", _timeProvider.GetUtcNow());
}
await cmd.ExecuteNonQueryAsync(ct);
// Audit log
await InsertAuditLogAsync(conn, goldenSetId, "status_changed",
reviewedBy ?? "system", currentStatus, status.ToString(), null, ct);
await tx.CommitAsync(ct);
_logger.LogInformation("Updated golden set {Id} status from {OldStatus} to {NewStatus}",
goldenSetId, currentStatus, status);
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
}
/// <inheritdoc />
public async Task<ImmutableArray<GoldenSetDefinition>> GetByComponentAsync(
string component,
GoldenSetStatus? statusFilter = GoldenSetStatus.Approved,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(component);
var sql = statusFilter.HasValue
? """
SELECT definition_yaml
FROM golden_sets.definitions
WHERE component = @component AND status = @status
ORDER BY created_at DESC
"""
: """
SELECT definition_yaml
FROM golden_sets.definitions
WHERE component = @component
ORDER BY created_at DESC
""";
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@component", component);
if (statusFilter.HasValue)
{
cmd.Parameters.AddWithValue("@status", statusFilter.Value.ToString().ToLowerInvariant());
}
var results = ImmutableArray.CreateBuilder<GoldenSetDefinition>();
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
var yaml = reader.GetString(0);
results.Add(GoldenSetYamlSerializer.Deserialize(yaml));
}
return results.ToImmutable();
}
/// <inheritdoc />
public async Task<bool> DeleteAsync(string goldenSetId, CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(goldenSetId);
// Soft delete - move to archived status
const string sql = """
UPDATE golden_sets.definitions
SET status = 'archived'
WHERE id = @id AND status != 'archived'
""";
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", goldenSetId);
var affected = await cmd.ExecuteNonQueryAsync(ct);
return affected > 0;
}
private async Task<bool> UpsertDefinitionAsync(
NpgsqlConnection conn,
GoldenSetDefinition definition,
GoldenSetStatus status,
string yaml,
string json,
string digest,
CancellationToken ct)
{
const string sql = """
INSERT INTO golden_sets.definitions
(id, component, content_digest, status, definition_yaml, definition_json,
target_count, author_id, created_at, source_ref, tags, schema_version)
VALUES
(@id, @component, @digest, @status, @yaml, @json::jsonb,
@target_count, @author_id, @created_at, @source_ref, @tags, @schema_version)
ON CONFLICT (id) DO UPDATE SET
component = EXCLUDED.component,
content_digest = EXCLUDED.content_digest,
status = EXCLUDED.status,
definition_yaml = EXCLUDED.definition_yaml,
definition_json = EXCLUDED.definition_json,
target_count = EXCLUDED.target_count,
source_ref = EXCLUDED.source_ref,
tags = EXCLUDED.tags,
schema_version = EXCLUDED.schema_version
RETURNING (xmax = 0) AS was_inserted
""";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", definition.Id);
cmd.Parameters.AddWithValue("@component", definition.Component);
cmd.Parameters.AddWithValue("@digest", digest);
cmd.Parameters.AddWithValue("@status", status.ToString().ToLowerInvariant());
cmd.Parameters.AddWithValue("@yaml", yaml);
cmd.Parameters.AddWithValue("@json", json);
cmd.Parameters.AddWithValue("@target_count", definition.Targets.Length);
cmd.Parameters.AddWithValue("@author_id", definition.Metadata.AuthorId);
cmd.Parameters.AddWithValue("@created_at", definition.Metadata.CreatedAt);
cmd.Parameters.AddWithValue("@source_ref", definition.Metadata.SourceRef);
cmd.Parameters.AddWithValue("@tags", definition.Metadata.Tags.ToArray());
cmd.Parameters.AddWithValue("@schema_version", definition.Metadata.SchemaVersion);
var wasInserted = (bool)(await cmd.ExecuteScalarAsync(ct) ?? false);
return !wasInserted; // Return true if was updated (not inserted)
}
private static async Task DeleteTargetsAsync(NpgsqlConnection conn, string goldenSetId, CancellationToken ct)
{
const string sql = "DELETE FROM golden_sets.targets WHERE golden_set_id = @id";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", goldenSetId);
await cmd.ExecuteNonQueryAsync(ct);
}
private static async Task InsertTargetsAsync(
NpgsqlConnection conn,
GoldenSetDefinition definition,
CancellationToken ct)
{
const string sql = """
INSERT INTO golden_sets.targets
(golden_set_id, function_name, edges, sinks, constants, taint_invariant, source_file, source_line)
VALUES
(@golden_set_id, @function_name, @edges::jsonb, @sinks, @constants, @taint_invariant, @source_file, @source_line)
""";
foreach (var target in definition.Targets)
{
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@golden_set_id", definition.Id);
cmd.Parameters.AddWithValue("@function_name", target.FunctionName);
cmd.Parameters.AddWithValue("@edges", JsonSerializer.Serialize(target.Edges.Select(e => e.ToString()).ToArray()));
cmd.Parameters.AddWithValue("@sinks", target.Sinks.ToArray());
cmd.Parameters.AddWithValue("@constants", target.Constants.ToArray());
cmd.Parameters.AddWithValue("@taint_invariant", (object?)target.TaintInvariant ?? DBNull.Value);
cmd.Parameters.AddWithValue("@source_file", (object?)target.SourceFile ?? DBNull.Value);
cmd.Parameters.AddWithValue("@source_line", (object?)target.SourceLine ?? DBNull.Value);
await cmd.ExecuteNonQueryAsync(ct);
}
}
private static async Task<string?> GetCurrentStatusAsync(
NpgsqlConnection conn,
string goldenSetId,
CancellationToken ct)
{
const string sql = "SELECT status FROM golden_sets.definitions WHERE id = @id";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", goldenSetId);
var result = await cmd.ExecuteScalarAsync(ct);
return result as string;
}
private async Task InsertAuditLogAsync(
NpgsqlConnection conn,
string goldenSetId,
string action,
string actorId,
string? oldStatus,
string? newStatus,
object? details,
CancellationToken ct)
{
const string sql = """
INSERT INTO golden_sets.audit_log
(golden_set_id, action, actor_id, old_status, new_status, details, timestamp)
VALUES
(@golden_set_id, @action, @actor_id, @old_status, @new_status, @details::jsonb, @timestamp)
""";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@golden_set_id", goldenSetId);
cmd.Parameters.AddWithValue("@action", action);
cmd.Parameters.AddWithValue("@actor_id", actorId);
cmd.Parameters.AddWithValue("@old_status", (object?)oldStatus ?? DBNull.Value);
cmd.Parameters.AddWithValue("@new_status", (object?)newStatus ?? DBNull.Value);
cmd.Parameters.AddWithValue("@details", details is null ? DBNull.Value : JsonSerializer.Serialize(details));
cmd.Parameters.AddWithValue("@timestamp", _timeProvider.GetUtcNow());
await cmd.ExecuteNonQueryAsync(ct);
}
/// <inheritdoc />
public async Task<GoldenSetStoreResult> UpdateStatusAsync(
string goldenSetId,
GoldenSetStatus status,
string actorId,
string comment,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(goldenSetId);
ArgumentException.ThrowIfNullOrWhiteSpace(actorId);
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var tx = await conn.BeginTransactionAsync(ct);
try
{
// Get current status
var currentStatus = await GetCurrentStatusAsync(conn, goldenSetId, ct);
if (currentStatus is null)
{
return GoldenSetStoreResult.Failed($"Golden set {goldenSetId} not found");
}
// Update status
var sql = status is GoldenSetStatus.Approved or GoldenSetStatus.InReview
? """
UPDATE golden_sets.definitions
SET status = @status, reviewed_by = @reviewed_by, reviewed_at = @reviewed_at
WHERE id = @id
"""
: """
UPDATE golden_sets.definitions
SET status = @status
WHERE id = @id
""";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", goldenSetId);
cmd.Parameters.AddWithValue("@status", status.ToString().ToLowerInvariant());
if (status is GoldenSetStatus.Approved or GoldenSetStatus.InReview)
{
cmd.Parameters.AddWithValue("@reviewed_by", actorId);
cmd.Parameters.AddWithValue("@reviewed_at", _timeProvider.GetUtcNow());
}
await cmd.ExecuteNonQueryAsync(ct);
// Audit log with comment
await InsertAuditLogWithCommentAsync(conn, goldenSetId, "status_change",
actorId, currentStatus, status.ToString().ToLowerInvariant(), comment, ct);
await tx.CommitAsync(ct);
_logger.LogInformation("Updated golden set {Id} status from {OldStatus} to {NewStatus} by {Actor}",
goldenSetId, currentStatus, status, actorId);
// Get the content digest to return
var digest = await GetContentDigestAsync(conn, goldenSetId, ct);
return GoldenSetStoreResult.Succeeded(digest ?? string.Empty, wasUpdated: true);
}
catch (Exception ex)
{
await tx.RollbackAsync(ct);
_logger.LogError(ex, "Failed to update status for golden set {Id}", goldenSetId);
return GoldenSetStoreResult.Failed(ex.Message);
}
}
/// <inheritdoc />
public async Task<StoredGoldenSet?> GetAsync(string goldenSetId, CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(goldenSetId);
const string sql = """
SELECT definition_yaml, status, created_at, COALESCE(reviewed_at, created_at) as updated_at
FROM golden_sets.definitions
WHERE id = @id
""";
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", goldenSetId);
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (!await reader.ReadAsync(ct))
{
return null;
}
var yaml = reader.GetString(0);
var status = Enum.Parse<GoldenSetStatus>(reader.GetString(1), ignoreCase: true);
var createdAt = reader.GetFieldValue<DateTimeOffset>(2);
var updatedAt = reader.GetFieldValue<DateTimeOffset>(3);
return new StoredGoldenSet
{
Definition = GoldenSetYamlSerializer.Deserialize(yaml),
Status = status,
CreatedAt = createdAt,
UpdatedAt = updatedAt
};
}
/// <inheritdoc />
public async Task<ImmutableArray<GoldenSetAuditEntry>> GetAuditLogAsync(
string goldenSetId,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(goldenSetId);
const string sql = """
SELECT action, actor_id, timestamp, old_status, new_status,
COALESCE(details->>'comment', '') as comment
FROM golden_sets.audit_log
WHERE golden_set_id = @id
ORDER BY timestamp DESC
""";
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", goldenSetId);
var results = ImmutableArray.CreateBuilder<GoldenSetAuditEntry>();
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
var oldStatusStr = reader.IsDBNull(3) ? null : reader.GetString(3);
var newStatusStr = reader.IsDBNull(4) ? null : reader.GetString(4);
results.Add(new GoldenSetAuditEntry
{
Operation = reader.GetString(0),
ActorId = reader.GetString(1),
Timestamp = reader.GetFieldValue<DateTimeOffset>(2),
OldStatus = string.IsNullOrEmpty(oldStatusStr) ? null : Enum.Parse<GoldenSetStatus>(oldStatusStr, ignoreCase: true),
NewStatus = string.IsNullOrEmpty(newStatusStr) ? null : Enum.Parse<GoldenSetStatus>(newStatusStr, ignoreCase: true),
Comment = reader.IsDBNull(5) ? null : reader.GetString(5)
});
}
return results.ToImmutable();
}
private static async Task<string?> GetContentDigestAsync(
NpgsqlConnection conn,
string goldenSetId,
CancellationToken ct)
{
const string sql = "SELECT content_digest FROM golden_sets.definitions WHERE id = @id";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", goldenSetId);
var result = await cmd.ExecuteScalarAsync(ct);
return result as string;
}
private async Task InsertAuditLogWithCommentAsync(
NpgsqlConnection conn,
string goldenSetId,
string action,
string actorId,
string? oldStatus,
string? newStatus,
string? comment,
CancellationToken ct)
{
const string sql = """
INSERT INTO golden_sets.audit_log
(golden_set_id, action, actor_id, old_status, new_status, details, timestamp)
VALUES
(@golden_set_id, @action, @actor_id, @old_status, @new_status, @details::jsonb, @timestamp)
""";
var details = comment is not null ? new { comment } : null;
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@golden_set_id", goldenSetId);
cmd.Parameters.AddWithValue("@action", action);
cmd.Parameters.AddWithValue("@actor_id", actorId);
cmd.Parameters.AddWithValue("@old_status", (object?)oldStatus ?? DBNull.Value);
cmd.Parameters.AddWithValue("@new_status", (object?)newStatus ?? DBNull.Value);
cmd.Parameters.AddWithValue("@details", details is null ? DBNull.Value : JsonSerializer.Serialize(details));
cmd.Parameters.AddWithValue("@timestamp", _timeProvider.GetUtcNow());
await cmd.ExecuteNonQueryAsync(ct);
}
}