synergy moats product advisory implementations

This commit is contained in:
master
2026-01-17 01:30:03 +02:00
parent 77ff029205
commit 702a27ac83
112 changed files with 21356 additions and 127 deletions

View File

@@ -0,0 +1,339 @@
// -----------------------------------------------------------------------------
// BlockExplanationController.cs
// Sprint: SPRINT_20260117_026_CLI_why_blocked_command
// Task: WHY-001 - Backend API for Block Explanation
// Description: API endpoint to retrieve block explanation for an artifact
// -----------------------------------------------------------------------------
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace StellaOps.Api.Controllers;
/// <summary>
/// Controller for artifact block explanation endpoints.
/// </summary>
[ApiController]
[Route("v1/artifacts")]
[Authorize]
public class BlockExplanationController : ControllerBase
{
private readonly IBlockExplanationService _explanationService;
private readonly ILogger<BlockExplanationController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="BlockExplanationController"/> class.
/// </summary>
public BlockExplanationController(
IBlockExplanationService explanationService,
ILogger<BlockExplanationController> logger)
{
_explanationService = explanationService;
_logger = logger;
}
/// <summary>
/// Gets the block explanation for an artifact.
/// </summary>
/// <param name="digest">The artifact digest (e.g., sha256:abc123...).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The block explanation or NotFound if artifact is not blocked.</returns>
/// <response code="200">Returns the block explanation.</response>
/// <response code="404">Artifact not found or not blocked.</response>
[HttpGet("{digest}/block-explanation")]
[ProducesResponseType(typeof(BlockExplanationResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetBlockExplanation(
[FromRoute] string digest,
CancellationToken ct)
{
_logger.LogDebug("Getting block explanation for artifact {Digest}", digest);
var explanation = await _explanationService.GetBlockExplanationAsync(digest, ct);
if (explanation == null)
{
return NotFound(new ProblemDetails
{
Title = "Artifact not blocked",
Detail = $"Artifact {digest} is not blocked or does not exist",
Status = StatusCodes.Status404NotFound
});
}
return Ok(explanation);
}
/// <summary>
/// Gets the block explanation with full evidence details.
/// </summary>
/// <param name="digest">The artifact digest.</param>
/// <param name="includeTrace">Whether to include policy evaluation trace.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The detailed block explanation.</returns>
[HttpGet("{digest}/block-explanation/detailed")]
[ProducesResponseType(typeof(DetailedBlockExplanationResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetDetailedBlockExplanation(
[FromRoute] string digest,
[FromQuery] bool includeTrace = false,
CancellationToken ct = default)
{
_logger.LogDebug("Getting detailed block explanation for artifact {Digest}", digest);
var explanation = await _explanationService.GetDetailedBlockExplanationAsync(
digest, includeTrace, ct);
if (explanation == null)
{
return NotFound(new ProblemDetails
{
Title = "Artifact not blocked",
Detail = $"Artifact {digest} is not blocked or does not exist",
Status = StatusCodes.Status404NotFound
});
}
return Ok(explanation);
}
}
/// <summary>
/// Response model for block explanation.
/// </summary>
public sealed record BlockExplanationResponse
{
/// <summary>
/// The artifact digest.
/// </summary>
public required string ArtifactDigest { get; init; }
/// <summary>
/// Whether the artifact is blocked.
/// </summary>
public bool IsBlocked { get; init; } = true;
/// <summary>
/// The gate that blocked the artifact.
/// </summary>
public required GateDecision GateDecision { get; init; }
/// <summary>
/// Evidence artifact references.
/// </summary>
public required IReadOnlyList<EvidenceReference> EvidenceReferences { get; init; }
/// <summary>
/// Replay token for deterministic verification.
/// </summary>
public required string ReplayToken { get; init; }
/// <summary>
/// Timestamp when the block decision was made.
/// </summary>
public DateTimeOffset BlockedAt { get; init; }
/// <summary>
/// Verdict ID for reference.
/// </summary>
public string? VerdictId { get; init; }
}
/// <summary>
/// Detailed block explanation with full evidence.
/// </summary>
public sealed record DetailedBlockExplanationResponse : BlockExplanationResponse
{
/// <summary>
/// Full policy evaluation trace.
/// </summary>
public PolicyEvaluationTrace? EvaluationTrace { get; init; }
/// <summary>
/// Full evidence details.
/// </summary>
public IReadOnlyList<EvidenceDetail>? EvidenceDetails { get; init; }
}
/// <summary>
/// Gate decision details.
/// </summary>
public sealed record GateDecision
{
/// <summary>
/// Gate identifier.
/// </summary>
public required string GateId { get; init; }
/// <summary>
/// Gate display name.
/// </summary>
public required string GateName { get; init; }
/// <summary>
/// Decision status.
/// </summary>
public required string Status { get; init; }
/// <summary>
/// Human-readable reason for the decision.
/// </summary>
public required string Reason { get; init; }
/// <summary>
/// Suggested remediation action.
/// </summary>
public string? Suggestion { get; init; }
/// <summary>
/// Policy version used.
/// </summary>
public string? PolicyVersion { get; init; }
/// <summary>
/// Threshold that was not met (if applicable).
/// </summary>
public ThresholdInfo? Threshold { get; init; }
}
/// <summary>
/// Threshold information for gate decisions.
/// </summary>
public sealed record ThresholdInfo
{
/// <summary>
/// Threshold name.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Required threshold value.
/// </summary>
public required double Required { get; init; }
/// <summary>
/// Actual value observed.
/// </summary>
public required double Actual { get; init; }
/// <summary>
/// Comparison operator.
/// </summary>
public required string Operator { get; init; }
}
/// <summary>
/// Reference to an evidence artifact.
/// </summary>
public sealed record EvidenceReference
{
/// <summary>
/// Evidence type.
/// </summary>
public required string Type { get; init; }
/// <summary>
/// Content-addressed ID.
/// </summary>
public required string ContentId { get; init; }
/// <summary>
/// Evidence source.
/// </summary>
public required string Source { get; init; }
/// <summary>
/// Timestamp when evidence was collected.
/// </summary>
public DateTimeOffset CollectedAt { get; init; }
/// <summary>
/// CLI command to retrieve this evidence.
/// </summary>
public string? RetrievalCommand { get; init; }
}
/// <summary>
/// Full evidence details.
/// </summary>
public sealed record EvidenceDetail : EvidenceReference
{
/// <summary>
/// Evidence content (JSON).
/// </summary>
public object? Content { get; init; }
/// <summary>
/// Content size in bytes.
/// </summary>
public long? SizeBytes { get; init; }
}
/// <summary>
/// Policy evaluation trace.
/// </summary>
public sealed record PolicyEvaluationTrace
{
/// <summary>
/// Trace ID.
/// </summary>
public required string TraceId { get; init; }
/// <summary>
/// Evaluation steps.
/// </summary>
public required IReadOnlyList<EvaluationStep> Steps { get; init; }
/// <summary>
/// Total evaluation duration.
/// </summary>
public TimeSpan Duration { get; init; }
}
/// <summary>
/// Single evaluation step.
/// </summary>
public sealed record EvaluationStep
{
/// <summary>
/// Step index.
/// </summary>
public int Index { get; init; }
/// <summary>
/// Gate ID evaluated.
/// </summary>
public required string GateId { get; init; }
/// <summary>
/// Input values.
/// </summary>
public object? Inputs { get; init; }
/// <summary>
/// Output decision.
/// </summary>
public required string Decision { get; init; }
/// <summary>
/// Step duration.
/// </summary>
public TimeSpan Duration { get; init; }
}
/// <summary>
/// Service interface for block explanations.
/// </summary>
public interface IBlockExplanationService
{
/// <summary>
/// Gets the block explanation for an artifact.
/// </summary>
Task<BlockExplanationResponse?> GetBlockExplanationAsync(string digest, CancellationToken ct);
/// <summary>
/// Gets detailed block explanation with full evidence.
/// </summary>
Task<DetailedBlockExplanationResponse?> GetDetailedBlockExplanationAsync(
string digest, bool includeTrace, CancellationToken ct);
}