Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -0,0 +1,12 @@
{
"profiles": {
"StellaOps.Replay.WebService": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:62536;http://localhost:62538"
}
}
}

View File

@@ -7,12 +7,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Sinks.Console" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Audit.ReplayToken\StellaOps.Audit.ReplayToken.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.AuditPack\StellaOps.AuditPack.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Configuration\StellaOps.Configuration.csproj" />

View File

@@ -0,0 +1,333 @@
// -----------------------------------------------------------------------------
// VerdictReplayEndpoints.cs
// Sprint: SPRINT_1227_0005_0004_BE_verdict_replay
// Task: T5 — Verdict replay API endpoints
// -----------------------------------------------------------------------------
using Microsoft.AspNetCore.Http.HttpResults;
using StellaOps.AuditPack.Models;
using StellaOps.AuditPack.Services;
namespace StellaOps.Replay.WebService;
/// <summary>
/// Extension methods for registering verdict replay endpoints.
/// </summary>
public static class VerdictReplayEndpoints
{
private const string ReplayVerdictPolicy = "replay.verdict";
/// <summary>
/// Maps verdict replay endpoints to the application.
/// </summary>
public static void MapVerdictReplayEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/v1/replay/verdict")
.WithTags("Verdict Replay");
// POST /v1/replay/verdict - Execute verdict replay
group.MapPost("/", ExecuteReplayAsync)
.WithName("ExecuteVerdictReplay")
.Produces<VerdictReplayResponse>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status404NotFound);
// POST /v1/replay/verdict/verify - Verify replay eligibility
group.MapPost("/verify", VerifyEligibilityAsync)
.WithName("VerifyReplayEligibility")
.Produces<ReplayEligibilityResponse>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
// GET /v1/replay/verdict/{manifestId}/status - Get replay status
group.MapGet("/{manifestId}/status", GetReplayStatusAsync)
.WithName("GetReplayStatus")
.Produces<ReplayStatusResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
// POST /v1/replay/verdict/compare - Compare two replay executions
group.MapPost("/compare", CompareReplayResultsAsync)
.WithName("CompareReplayResults")
.Produces<ReplayComparisonResponse>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
}
private static async Task<Results<Ok<VerdictReplayResponse>, NotFound, ProblemHttpResult>> ExecuteReplayAsync(
HttpContext httpContext,
VerdictReplayRequest request,
IReplayExecutor replayExecutor,
IAuditBundleReader bundleReader,
IVerdictReplayPredicate replayPredicate,
CancellationToken ct)
{
// Read the audit bundle
var readRequest = new AuditBundleReadRequest { BundlePath = request.BundlePath };
var bundleResult = await bundleReader.ReadAsync(readRequest, ct);
if (!bundleResult.Success || bundleResult.Manifest is null)
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "bundle_read_failed",
detail: bundleResult.Error ?? "Failed to read audit bundle");
}
// Check eligibility
var eligibility = replayPredicate.Evaluate(bundleResult.Manifest, request.CurrentInputState);
if (!eligibility.IsEligible)
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "replay_not_eligible",
detail: string.Join("; ", eligibility.Reasons));
}
// Create isolated context and execute replay
var options = new IsolatedReplayContextOptions
{
CleanupOnDispose = true,
EnforceOffline = true,
EvaluationTime = request.EvaluationTime
};
using var context = new IsolatedReplayContext(options);
var initResult = await context.InitializeAsync(bundleResult, ct);
if (!initResult.Success)
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "context_init_failed",
detail: initResult.Error ?? "Failed to initialize replay context");
}
var execOptions = new ReplayExecutionOptions
{
FailOnInputDrift = request.FailOnInputDrift,
DetailedDriftDetection = request.DetailedDriftDetection,
StrictMode = request.StrictMode
};
var result = await replayExecutor.ExecuteAsync(context, bundleResult.Manifest, execOptions, ct);
// Generate divergence report if needed
ReplayDivergenceReport? divergenceReport = null;
if (result.Success && !result.VerdictMatches)
{
var originalResult = new ReplayExecutionResult
{
Success = true,
OriginalVerdictDigest = bundleResult.Manifest.VerdictDigest,
OriginalDecision = bundleResult.Manifest.Decision
};
divergenceReport = replayPredicate.CompareDivergence(originalResult, result);
}
return TypedResults.Ok(new VerdictReplayResponse
{
Success = result.Success,
Status = result.Status.ToString(),
VerdictMatches = result.VerdictMatches,
DecisionMatches = result.DecisionMatches,
OriginalVerdictDigest = result.OriginalVerdictDigest,
ReplayedVerdictDigest = result.ReplayedVerdictDigest,
OriginalDecision = result.OriginalDecision,
ReplayedDecision = result.ReplayedDecision,
Drifts = result.Drifts.Select(d => new DriftItemDto
{
Type = d.Type.ToString(),
Field = d.Field,
Expected = d.Expected,
Actual = d.Actual,
Message = d.Message
}).ToList(),
DivergenceSummary = divergenceReport?.Summary,
DurationMs = result.DurationMs,
EvaluatedAt = result.EvaluatedAt,
Error = result.Error
});
}
private static async Task<Results<Ok<ReplayEligibilityResponse>, ProblemHttpResult>> VerifyEligibilityAsync(
HttpContext httpContext,
VerifyEligibilityRequest request,
IAuditBundleReader bundleReader,
IVerdictReplayPredicate replayPredicate,
CancellationToken ct)
{
var readRequest = new AuditBundleReadRequest { BundlePath = request.BundlePath };
var bundleResult = await bundleReader.ReadAsync(readRequest, ct);
if (!bundleResult.Success || bundleResult.Manifest is null)
{
return TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "bundle_read_failed",
detail: bundleResult.Error ?? "Failed to read audit bundle");
}
var eligibility = replayPredicate.Evaluate(bundleResult.Manifest, request.CurrentInputState);
return TypedResults.Ok(new ReplayEligibilityResponse
{
IsEligible = eligibility.IsEligible,
Reasons = eligibility.Reasons.ToList(),
Warnings = eligibility.Warnings.ToList(),
ConfidenceScore = eligibility.ConfidenceScore,
ExpectedStatus = eligibility.ExpectedOutcome?.ExpectedStatus.ToString(),
ExpectedDecision = eligibility.ExpectedOutcome?.ExpectedDecision,
Rationale = eligibility.ExpectedOutcome?.Rationale
});
}
private static Task<Results<Ok<ReplayStatusResponse>, NotFound>> GetReplayStatusAsync(
string manifestId,
CancellationToken ct)
{
// In production, this would look up stored replay results
// For now, return a placeholder
return Task.FromResult<Results<Ok<ReplayStatusResponse>, NotFound>>(
TypedResults.Ok(new ReplayStatusResponse
{
ManifestId = manifestId,
LastReplayedAt = null,
TotalReplays = 0,
SuccessfulReplays = 0,
FailedReplays = 0,
Status = "not_replayed"
}));
}
private static Task<Results<Ok<ReplayComparisonResponse>, ProblemHttpResult>> CompareReplayResultsAsync(
HttpContext httpContext,
ReplayComparisonRequest request,
IVerdictReplayPredicate replayPredicate,
CancellationToken ct)
{
// Create execution results from the request data
var original = new ReplayExecutionResult
{
Success = true,
OriginalVerdictDigest = request.OriginalVerdictDigest,
OriginalDecision = request.OriginalDecision
};
var replayed = new ReplayExecutionResult
{
Success = true,
ReplayedVerdictDigest = request.ReplayedVerdictDigest,
ReplayedDecision = request.ReplayedDecision,
Drifts = request.Drifts?.Select(d => new DriftItem
{
Type = Enum.TryParse<DriftType>(d.Type, out var t) ? t : DriftType.Other,
Field = d.Field,
Expected = d.Expected,
Actual = d.Actual,
Message = d.Message
}).ToList() ?? []
};
var report = replayPredicate.CompareDivergence(original, replayed);
return Task.FromResult<Results<Ok<ReplayComparisonResponse>, ProblemHttpResult>>(
TypedResults.Ok(new ReplayComparisonResponse
{
HasDivergence = report.HasDivergence,
OverallSeverity = report.OverallSeverity.ToString(),
Summary = report.Summary,
Divergences = report.Divergences.Select(d => new DivergenceItemDto
{
Category = d.Category.ToString(),
Field = d.Field,
OriginalValue = d.OriginalValue,
ReplayedValue = d.ReplayedValue,
Severity = d.Severity.ToString(),
Explanation = d.Explanation
}).ToList()
}));
}
}
#region Request/Response DTOs
public record VerdictReplayRequest(
string BundlePath,
ReplayInputState? CurrentInputState = null,
DateTimeOffset? EvaluationTime = null,
bool FailOnInputDrift = false,
bool DetailedDriftDetection = true,
bool StrictMode = false);
public record VerdictReplayResponse
{
public bool Success { get; init; }
public required string Status { get; init; }
public bool VerdictMatches { get; init; }
public bool DecisionMatches { get; init; }
public string? OriginalVerdictDigest { get; init; }
public string? ReplayedVerdictDigest { get; init; }
public string? OriginalDecision { get; init; }
public string? ReplayedDecision { get; init; }
public required List<DriftItemDto> Drifts { get; init; }
public string? DivergenceSummary { get; init; }
public long DurationMs { get; init; }
public DateTimeOffset EvaluatedAt { get; init; }
public string? Error { get; init; }
}
public record DriftItemDto
{
public required string Type { get; init; }
public string? Field { get; init; }
public string? Expected { get; init; }
public string? Actual { get; init; }
public string? Message { get; init; }
}
public record VerifyEligibilityRequest(
string BundlePath,
ReplayInputState? CurrentInputState = null);
public record ReplayEligibilityResponse
{
public bool IsEligible { get; init; }
public required List<string> Reasons { get; init; }
public required List<string> Warnings { get; init; }
public double ConfidenceScore { get; init; }
public string? ExpectedStatus { get; init; }
public string? ExpectedDecision { get; init; }
public string? Rationale { get; init; }
}
public record ReplayStatusResponse
{
public required string ManifestId { get; init; }
public DateTimeOffset? LastReplayedAt { get; init; }
public int TotalReplays { get; init; }
public int SuccessfulReplays { get; init; }
public int FailedReplays { get; init; }
public required string Status { get; init; }
}
public record ReplayComparisonRequest(
string OriginalVerdictDigest,
string OriginalDecision,
string ReplayedVerdictDigest,
string ReplayedDecision,
List<DriftItemDto>? Drifts = null);
public record ReplayComparisonResponse
{
public bool HasDivergence { get; init; }
public required string OverallSeverity { get; init; }
public string? Summary { get; init; }
public required List<DivergenceItemDto> Divergences { get; init; }
}
public record DivergenceItemDto
{
public required string Category { get; init; }
public required string Field { get; init; }
public string? OriginalValue { get; init; }
public string? ReplayedValue { get; init; }
public required string Severity { get; init; }
public string? Explanation { get; init; }
}
#endregion