up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-24 07:52:25 +02:00
parent 5970f0d9bd
commit 150b3730ef
215 changed files with 8119 additions and 740 deletions

View File

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
using StellaOps.Policy.Engine.Ledger;
namespace StellaOps.Policy.Engine.Snapshots;
internal sealed record SnapshotSummary(
[property: JsonPropertyName("snapshot_id")] string SnapshotId,
[property: JsonPropertyName("tenant_id")] string TenantId,
[property: JsonPropertyName("ledger_export_id")] string LedgerExportId,
[property: JsonPropertyName("generated_at")] string GeneratedAt,
[property: JsonPropertyName("status_counts")] IReadOnlyDictionary<string, int> StatusCounts);
internal sealed record SnapshotDetail(
[property: JsonPropertyName("snapshot_id")] string SnapshotId,
[property: JsonPropertyName("tenant_id")] string TenantId,
[property: JsonPropertyName("ledger_export_id")] string LedgerExportId,
[property: JsonPropertyName("generated_at")] string GeneratedAt,
[property: JsonPropertyName("overlay_hash")] string OverlayHash,
[property: JsonPropertyName("status_counts")] IReadOnlyDictionary<string, int> StatusCounts,
[property: JsonPropertyName("records")] IReadOnlyList<LedgerExportRecord> Records);
internal sealed record SnapshotRequest(
[property: JsonPropertyName("tenant_id")] string TenantId,
[property: JsonPropertyName("overlay_hash")] string OverlayHash);

View File

@@ -0,0 +1,76 @@
using StellaOps.Policy.Engine.Ledger;
using StellaOps.Policy.Engine.Orchestration;
namespace StellaOps.Policy.Engine.Snapshots;
/// <summary>
/// Snapshot API stub (POLICY-ENGINE-35-201) built on ledger exports.
/// </summary>
internal sealed class SnapshotService
{
private readonly TimeProvider _timeProvider;
private readonly LedgerExportService _ledger;
private readonly ISnapshotStore _store;
public SnapshotService(
TimeProvider timeProvider,
LedgerExportService ledger,
ISnapshotStore store)
{
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_ledger = ledger ?? throw new ArgumentNullException(nameof(ledger));
_store = store ?? throw new ArgumentNullException(nameof(store));
}
public async Task<SnapshotDetail> CreateAsync(
SnapshotRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
var exports = await _ledger.ListAsync(request.TenantId, cancellationToken).ConfigureAwait(false);
var export = exports.LastOrDefault();
if (export is null)
{
export = await _ledger.BuildAsync(new LedgerExportRequest(request.TenantId), cancellationToken)
.ConfigureAwait(false);
}
var statusCounts = export.Records
.GroupBy(r => r.Status)
.ToDictionary(g => g.Key, g => g.Count(), StringComparer.Ordinal);
var generatedAt = _timeProvider.GetUtcNow().ToString("O");
var snapshotId = StableIdGenerator.CreateUlid($"{export.Manifest.ExportId}|{request.OverlayHash}");
var snapshot = new SnapshotDetail(
SnapshotId: snapshotId,
TenantId: request.TenantId,
LedgerExportId: export.Manifest.ExportId,
GeneratedAt: generatedAt,
OverlayHash: request.OverlayHash,
StatusCounts: statusCounts,
Records: export.Records);
await _store.SaveAsync(snapshot, cancellationToken).ConfigureAwait(false);
return snapshot;
}
public Task<SnapshotDetail?> GetAsync(string snapshotId, CancellationToken cancellationToken = default)
{
return _store.GetAsync(snapshotId, cancellationToken);
}
public async Task<(IReadOnlyList<SnapshotSummary> Items, string? NextCursor)> ListAsync(
string? tenantId = null,
CancellationToken cancellationToken = default)
{
var snapshots = await _store.ListAsync(tenantId, cancellationToken).ConfigureAwait(false);
var summaries = snapshots
.OrderByDescending(s => s.GeneratedAt, StringComparer.Ordinal)
.Select(s => new SnapshotSummary(s.SnapshotId, s.TenantId, s.LedgerExportId, s.GeneratedAt, s.StatusCounts))
.ToList();
return (summaries, null);
}
}

View File

@@ -0,0 +1,44 @@
using System.Collections.Concurrent;
namespace StellaOps.Policy.Engine.Snapshots;
internal interface ISnapshotStore
{
Task SaveAsync(SnapshotDetail snapshot, CancellationToken cancellationToken = default);
Task<SnapshotDetail?> GetAsync(string snapshotId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<SnapshotDetail>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default);
}
internal sealed class InMemorySnapshotStore : ISnapshotStore
{
private readonly ConcurrentDictionary<string, SnapshotDetail> _snapshots = new(StringComparer.Ordinal);
public Task SaveAsync(SnapshotDetail snapshot, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(snapshot);
_snapshots[snapshot.SnapshotId] = snapshot;
return Task.CompletedTask;
}
public Task<SnapshotDetail?> GetAsync(string snapshotId, CancellationToken cancellationToken = default)
{
_snapshots.TryGetValue(snapshotId, out var value);
return Task.FromResult(value);
}
public Task<IReadOnlyList<SnapshotDetail>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
{
IEnumerable<SnapshotDetail> items = _snapshots.Values;
if (!string.IsNullOrWhiteSpace(tenantId))
{
items = items.Where(s => string.Equals(s.TenantId, tenantId, StringComparison.Ordinal));
}
var ordered = items
.OrderBy(s => s.GeneratedAt, StringComparer.Ordinal)
.ThenBy(s => s.SnapshotId, StringComparer.Ordinal)
.ToList();
return Task.FromResult<IReadOnlyList<SnapshotDetail>>(ordered);
}
}