using System.Collections.Immutable; using System.Text.Json; using System.Text.Json.Serialization; namespace StellaOps.Scanner.EntryTrace.Serialization; public static class EntryTraceGraphSerializer { private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false }; static EntryTraceGraphSerializer() { SerializerOptions.Converters.Add(new JsonStringEnumConverter()); } public static string Serialize(EntryTraceGraph graph) { ArgumentNullException.ThrowIfNull(graph); var contract = EntryTraceGraphContract.FromGraph(graph); return JsonSerializer.Serialize(contract, SerializerOptions); } public static EntryTraceGraph Deserialize(string json) { ArgumentException.ThrowIfNullOrWhiteSpace(json); var contract = JsonSerializer.Deserialize(json, SerializerOptions) ?? throw new InvalidOperationException("Failed to deserialize EntryTrace graph."); return contract.ToGraph(); } private sealed class EntryTraceGraphContract { public EntryTraceOutcome Outcome { get; set; } public List Nodes { get; set; } = new(); public List Edges { get; set; } = new(); public List Diagnostics { get; set; } = new(); public List Plans { get; set; } = new(); public List Terminals { get; set; } = new(); public static EntryTraceGraphContract FromGraph(EntryTraceGraph graph) { return new EntryTraceGraphContract { Outcome = graph.Outcome, Nodes = graph.Nodes.Select(EntryTraceNodeContract.FromNode).ToList(), Edges = graph.Edges.Select(EntryTraceEdgeContract.FromEdge).ToList(), Diagnostics = graph.Diagnostics.Select(EntryTraceDiagnosticContract.FromDiagnostic).ToList(), Plans = graph.Plans.Select(EntryTracePlanContract.FromPlan).ToList(), Terminals = graph.Terminals.Select(EntryTraceTerminalContract.FromTerminal).ToList() }; } public EntryTraceGraph ToGraph() { return new EntryTraceGraph( Outcome, Nodes.Select(n => n.ToNode()).ToImmutableArray(), Edges.Select(e => e.ToEdge()).ToImmutableArray(), Diagnostics.Select(d => d.ToDiagnostic()).ToImmutableArray(), Plans.Select(p => p.ToPlan()).ToImmutableArray(), Terminals.Select(t => t.ToTerminal()).ToImmutableArray()); } } private sealed class EntryTraceNodeContract { public int Id { get; set; } public EntryTraceNodeKind Kind { get; set; } public string DisplayName { get; set; } = string.Empty; public List Arguments { get; set; } = new(); public EntryTraceInterpreterKind InterpreterKind { get; set; } public EntryTraceEvidenceContract? Evidence { get; set; } public EntryTraceSpanContract? Span { get; set; } public Dictionary? Metadata { get; set; } public static EntryTraceNodeContract FromNode(EntryTraceNode node) { return new EntryTraceNodeContract { Id = node.Id, Kind = node.Kind, DisplayName = node.DisplayName, Arguments = node.Arguments.ToList(), InterpreterKind = node.InterpreterKind, Evidence = node.Evidence is null ? null : EntryTraceEvidenceContract.FromEvidence(node.Evidence), Span = node.Span is null ? null : EntryTraceSpanContract.FromSpan(node.Span.Value), Metadata = node.Metadata?.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal) }; } public EntryTraceNode ToNode() { return new EntryTraceNode( Id, Kind, DisplayName, Arguments.ToImmutableArray(), InterpreterKind, Evidence?.ToEvidence(), Span?.ToSpan(), Metadata is null ? null : Metadata.ToImmutableDictionary(StringComparer.Ordinal)); } } private sealed class EntryTraceEdgeContract { public int From { get; set; } public int To { get; set; } public string Relationship { get; set; } = string.Empty; public Dictionary? Metadata { get; set; } public static EntryTraceEdgeContract FromEdge(EntryTraceEdge edge) { return new EntryTraceEdgeContract { From = edge.FromNodeId, To = edge.ToNodeId, Relationship = edge.Relationship, Metadata = edge.Metadata?.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal) }; } public EntryTraceEdge ToEdge() { return new EntryTraceEdge( From, To, Relationship, Metadata is null ? null : Metadata.ToImmutableDictionary(StringComparer.Ordinal)); } } private sealed class EntryTraceDiagnosticContract { public EntryTraceDiagnosticSeverity Severity { get; set; } public EntryTraceUnknownReason Reason { get; set; } public string Message { get; set; } = string.Empty; public EntryTraceSpanContract? Span { get; set; } public string? RelatedPath { get; set; } public static EntryTraceDiagnosticContract FromDiagnostic(EntryTraceDiagnostic diagnostic) { return new EntryTraceDiagnosticContract { Severity = diagnostic.Severity, Reason = diagnostic.Reason, Message = diagnostic.Message, Span = diagnostic.Span is null ? null : EntryTraceSpanContract.FromSpan(diagnostic.Span.Value), RelatedPath = diagnostic.RelatedPath }; } public EntryTraceDiagnostic ToDiagnostic() { return new EntryTraceDiagnostic( Severity, Reason, Message, Span?.ToSpan(), RelatedPath); } } private sealed class EntryTracePlanContract { public List Command { get; set; } = new(); public Dictionary Environment { get; set; } = new(); public string WorkingDirectory { get; set; } = string.Empty; public string User { get; set; } = string.Empty; public string TerminalPath { get; set; } = string.Empty; public EntryTraceTerminalType Type { get; set; } public string? Runtime { get; set; } public double Confidence { get; set; } public Dictionary Evidence { get; set; } = new(); public static EntryTracePlanContract FromPlan(EntryTracePlan plan) { return new EntryTracePlanContract { Command = plan.Command.ToList(), Environment = plan.Environment.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal), WorkingDirectory = plan.WorkingDirectory, User = plan.User, TerminalPath = plan.TerminalPath, Type = plan.Type, Runtime = plan.Runtime, Confidence = plan.Confidence, Evidence = plan.Evidence.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal) }; } public EntryTracePlan ToPlan() { return new EntryTracePlan( Command.ToImmutableArray(), Environment.ToImmutableDictionary(StringComparer.Ordinal), WorkingDirectory, User, TerminalPath, Type, Runtime, Confidence, Evidence.ToImmutableDictionary(StringComparer.Ordinal)); } } private sealed class EntryTraceTerminalContract { public string Path { get; set; } = string.Empty; public EntryTraceTerminalType Type { get; set; } public string? Runtime { get; set; } public double Confidence { get; set; } public Dictionary Evidence { get; set; } = new(); public string User { get; set; } = string.Empty; public string WorkingDirectory { get; set; } = string.Empty; public List Arguments { get; set; } = new(); public static EntryTraceTerminalContract FromTerminal(EntryTraceTerminal terminal) { return new EntryTraceTerminalContract { Path = terminal.Path, Type = terminal.Type, Runtime = terminal.Runtime, Confidence = terminal.Confidence, Evidence = terminal.Evidence.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal), User = terminal.User, WorkingDirectory = terminal.WorkingDirectory, Arguments = terminal.Arguments.ToList() }; } public EntryTraceTerminal ToTerminal() { return new EntryTraceTerminal( Path, Type, Runtime, Confidence, Evidence.ToImmutableDictionary(StringComparer.Ordinal), User, WorkingDirectory, Arguments.ToImmutableArray()); } } private sealed class EntryTraceEvidenceContract { public string Path { get; set; } = string.Empty; public string? Layer { get; set; } public string? Source { get; set; } public Dictionary? Metadata { get; set; } public static EntryTraceEvidenceContract FromEvidence(EntryTraceEvidence evidence) { return new EntryTraceEvidenceContract { Path = evidence.Path, Layer = evidence.LayerDigest, Source = evidence.Source, Metadata = evidence.Metadata?.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal) }; } public EntryTraceEvidence ToEvidence() { return new EntryTraceEvidence( Path, Layer, Source ?? string.Empty, Metadata is null ? null : new Dictionary(Metadata, StringComparer.Ordinal)); } } private sealed class EntryTraceSpanContract { public string? Path { get; set; } public int StartLine { get; set; } public int StartColumn { get; set; } public int EndLine { get; set; } public int EndColumn { get; set; } public static EntryTraceSpanContract FromSpan(EntryTraceSpan span) { return new EntryTraceSpanContract { Path = span.Path, StartLine = span.StartLine, StartColumn = span.StartColumn, EndLine = span.EndLine, EndColumn = span.EndColumn }; } public EntryTraceSpan ToSpan() { return new EntryTraceSpan( Path, StartLine, StartColumn, EndLine, EndColumn); } } }