save progress

This commit is contained in:
StellaOps Bot
2025-12-18 09:10:36 +02:00
parent b4235c134c
commit 28823a8960
169 changed files with 11995 additions and 449 deletions

View File

@@ -0,0 +1,115 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Scheduler.Storage.Postgres.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.WebService.Auth;
namespace StellaOps.Scheduler.WebService.FailureSignatures;
internal static class FailureSignatureEndpoints
{
private const string ReadScope = "scheduler.runs.read";
public static IEndpointRouteBuilder MapFailureSignatureEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/v1/scheduler/failure-signatures");
group.MapGet("/best-match", GetBestMatchAsync);
return routes;
}
private static async Task<IResult> GetBestMatchAsync(
HttpContext httpContext,
[FromQuery] string? scopeType,
[FromQuery] string? scopeId,
[FromQuery] string? toolchainHash,
[FromServices] ITenantContextAccessor tenantAccessor,
[FromServices] IScopeAuthorizer scopeAuthorizer,
[FromServices] IServiceProvider serviceProvider,
CancellationToken cancellationToken)
{
try
{
scopeAuthorizer.EnsureScope(httpContext, ReadScope);
var tenant = tenantAccessor.GetTenant(httpContext);
if (string.IsNullOrWhiteSpace(scopeType))
{
throw new ValidationException("scopeType must be provided.");
}
if (string.IsNullOrWhiteSpace(scopeId))
{
throw new ValidationException("scopeId must be provided.");
}
if (!Enum.TryParse<FailureSignatureScopeType>(scopeType.Trim(), ignoreCase: true, out var parsedScopeType))
{
throw new ValidationException($"scopeType '{scopeType}' is not valid.");
}
var repository = serviceProvider.GetService<IFailureSignatureRepository>();
if (repository is null)
{
return Results.Problem(
detail: "Failure signature storage is not configured.",
statusCode: StatusCodes.Status503ServiceUnavailable);
}
var match = await repository
.GetBestMatchAsync(
tenant.TenantId,
parsedScopeType,
scopeId.Trim(),
string.IsNullOrWhiteSpace(toolchainHash) ? null : toolchainHash.Trim(),
cancellationToken)
.ConfigureAwait(false);
if (match is null)
{
return Results.NoContent();
}
return Results.Ok(new FailureSignatureBestMatchResponse(match));
}
catch (Exception ex) when (ex is ArgumentException or ValidationException)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private sealed record FailureSignatureBestMatchResponse
{
public FailureSignatureBestMatchResponse(FailureSignatureEntity signature)
{
ArgumentNullException.ThrowIfNull(signature);
SignatureId = signature.SignatureId;
ScopeType = signature.ScopeType.ToString().ToLowerInvariant();
ScopeId = signature.ScopeId;
ToolchainHash = signature.ToolchainHash;
ErrorCode = signature.ErrorCode;
ErrorCategory = signature.ErrorCategory?.ToString().ToLowerInvariant();
PredictedOutcome = signature.PredictedOutcome.ToString().ToLowerInvariant();
ConfidenceScore = signature.ConfidenceScore;
OccurrenceCount = signature.OccurrenceCount;
FirstSeenAt = signature.FirstSeenAt;
LastSeenAt = signature.LastSeenAt;
}
public Guid SignatureId { get; }
public string ScopeType { get; }
public string ScopeId { get; }
public string ToolchainHash { get; }
public string? ErrorCode { get; }
public string? ErrorCategory { get; }
public string PredictedOutcome { get; }
public decimal? ConfidenceScore { get; }
public int OccurrenceCount { get; }
public DateTimeOffset FirstSeenAt { get; }
public DateTimeOffset LastSeenAt { get; }
}
}

View File

@@ -245,8 +245,8 @@ internal static class PolicySimulationEndpointExtensions
var preview = new
{
candidates = inputs.Targets?.Count ?? 0,
estimatedRuns = inputs.Targets?.Count ?? 0,
candidates = inputs.SbomSet.Length,
estimatedRuns = inputs.SbomSet.Length,
message = "preview pending execution; actual diff will be available once job starts"
};

View File

@@ -8,11 +8,13 @@ using StellaOps.Plugin.DependencyInjection;
using StellaOps.Plugin.Hosting;
using StellaOps.Scheduler.WebService.Hosting;
using StellaOps.Scheduler.ImpactIndex;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.WebService;
using StellaOps.Scheduler.WebService.Auth;
using StellaOps.Scheduler.WebService.EventWebhooks;
using StellaOps.Scheduler.WebService.FailureSignatures;
using StellaOps.Scheduler.WebService.GraphJobs;
using StellaOps.Scheduler.WebService.GraphJobs.Events;
using StellaOps.Scheduler.WebService.Schedules;
@@ -213,6 +215,7 @@ app.MapGraphJobEndpoints();
ResolverJobEndpointExtensions.MapResolverJobEndpoints(app);
app.MapScheduleEndpoints();
app.MapRunEndpoints();
app.MapFailureSignatureEndpoints();
app.MapPolicyRunEndpoints();
app.MapPolicySimulationEndpoints();
app.MapSchedulerEventWebhookEndpoints();

View File

@@ -94,6 +94,17 @@ internal sealed class InMemoryRunRepository : IRunRepository
query = query.Where(run => run.CreatedAt > createdAfter);
}
if (options.Cursor is { } cursor)
{
query = options.SortAscending
? query.Where(run => run.CreatedAt > cursor.CreatedAt ||
(run.CreatedAt == cursor.CreatedAt &&
string.Compare(run.Id, cursor.RunId, StringComparison.Ordinal) > 0))
: query.Where(run => run.CreatedAt < cursor.CreatedAt ||
(run.CreatedAt == cursor.CreatedAt &&
string.Compare(run.Id, cursor.RunId, StringComparison.Ordinal) < 0));
}
query = options.SortAscending
? query.OrderBy(run => run.CreatedAt).ThenBy(run => run.Id, StringComparer.Ordinal)
: query.OrderByDescending(run => run.CreatedAt).ThenByDescending(run => run.Id, StringComparer.Ordinal);

View File

@@ -12,6 +12,7 @@ using StellaOps.Scheduler.ImpactIndex;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.WebService.Auth;
using StellaOps.Scheduler.WebService.Schedules;
namespace StellaOps.Scheduler.WebService.Runs;

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.WebService.Schedules;
public interface ISchedulerAuditService
{
Task<AuditRecord> WriteAsync(SchedulerAuditEvent auditEvent, CancellationToken cancellationToken = default);
}
public sealed record SchedulerAuditEvent(
string TenantId,
string Category,
string Action,
AuditActor Actor,
DateTimeOffset? OccurredAt = null,
string? AuditId = null,
string? EntityId = null,
string? ScheduleId = null,
string? RunId = null,
string? CorrelationId = null,
IReadOnlyDictionary<string, string>? Metadata = null,
string? Message = null);

View File

@@ -0,0 +1,6 @@
# Active Tasks
| ID | Status | Owner(s) | Depends on | Description | Notes |
|----|--------|----------|------------|-------------|-------|
| SCHED-WS-TTFS-0341-T4 | DONE (2025-12-18) | Agent | `docs/implplan/SPRINT_0341_0001_0001_ttfs_enhancements.md` | Add failure signature best-match endpoint to support TTFS FirstSignal enrichment. | `GET /api/v1/scheduler/failure-signatures/best-match` + deterministic endpoint tests. |