using System.Text.Json; using StellaOps.TaskRunner.Core.Execution; namespace StellaOps.TaskRunner.Infrastructure.Execution; public sealed class FilesystemPackRunArtifactReader : IPackRunArtifactReader { private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); private readonly string rootPath; public FilesystemPackRunArtifactReader(string rootPath) { ArgumentException.ThrowIfNullOrWhiteSpace(rootPath); this.rootPath = Path.GetFullPath(rootPath); } public async Task> ListAsync(string runId, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(runId); var manifestPath = Path.Combine(rootPath, Sanitize(runId), "artifact-manifest.json"); if (!File.Exists(manifestPath)) { return Array.Empty(); } await using var stream = File.OpenRead(manifestPath); var manifest = await JsonSerializer.DeserializeAsync(stream, SerializerOptions, cancellationToken) .ConfigureAwait(false); if (manifest is null || manifest.Outputs is null) { return Array.Empty(); } return manifest.Outputs .OrderBy(output => output.Name, StringComparer.Ordinal) .Select(output => new PackRunArtifactRecord( output.Name, output.Type, output.SourcePath, output.StoredPath, output.Status, output.Notes, manifest.UploadedAt, output.ExpressionJson)) .ToList(); } private static string Sanitize(string value) { var safe = value.Trim(); foreach (var invalid in Path.GetInvalidFileNameChars()) { safe = safe.Replace(invalid, '_'); } return string.IsNullOrWhiteSpace(safe) ? "run" : safe; } private sealed record ArtifactManifest( string RunId, DateTimeOffset UploadedAt, List Outputs); private sealed record ArtifactRecord( string Name, string Type, string? SourcePath, string? StoredPath, string Status, string? Notes, string? ExpressionJson); }