using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using StellaOps.Scanner.Reachability; namespace StellaOps.Reachability.FixtureTests.PatchOracle; /// /// Compares a RichGraph against a patch-oracle definition. /// Reports missing expected elements and present forbidden elements. /// public sealed class PatchOracleComparer { private readonly PatchOracleDefinition _oracle; private readonly double _defaultMinConfidence; public PatchOracleComparer(PatchOracleDefinition oracle) { _oracle = oracle ?? throw new ArgumentNullException(nameof(oracle)); _defaultMinConfidence = oracle.MinConfidence; } /// /// Compares the graph against the oracle and returns a result. /// public PatchOracleResult Compare(RichGraph graph) { ArgumentNullException.ThrowIfNull(graph); var violations = new List(); // Check expected functions foreach (var expected in _oracle.ExpectedFunctions.Where(f => f.Required)) { if (!HasMatchingNode(graph, expected)) { violations.Add(new PatchOracleViolation( ViolationType.MissingFunction, expected.SymbolId, null, expected.Reason ?? $"Expected function '{expected.SymbolId}' not found in graph")); } } // Check expected edges foreach (var expected in _oracle.ExpectedEdges.Where(e => e.Required)) { var minConf = expected.MinConfidence ?? _defaultMinConfidence; if (!HasMatchingEdge(graph, expected, minConf)) { violations.Add(new PatchOracleViolation( ViolationType.MissingEdge, expected.From, expected.To, expected.Reason ?? $"Expected edge '{expected.From}' -> '{expected.To}' not found in graph")); } } // Check expected roots foreach (var expected in _oracle.ExpectedRoots.Where(r => r.Required)) { if (!HasMatchingRoot(graph, expected)) { violations.Add(new PatchOracleViolation( ViolationType.MissingRoot, expected.Id, null, expected.Reason ?? $"Expected root '{expected.Id}' not found in graph")); } } // Check forbidden functions foreach (var forbidden in _oracle.ForbiddenFunctions) { if (HasMatchingNode(graph, forbidden)) { violations.Add(new PatchOracleViolation( ViolationType.ForbiddenFunctionPresent, forbidden.SymbolId, null, forbidden.Reason ?? $"Forbidden function '{forbidden.SymbolId}' is present in graph")); } } // Check forbidden edges foreach (var forbidden in _oracle.ForbiddenEdges) { if (HasMatchingEdge(graph, forbidden, 0.0)) { violations.Add(new PatchOracleViolation( ViolationType.ForbiddenEdgePresent, forbidden.From, forbidden.To, forbidden.Reason ?? $"Forbidden edge '{forbidden.From}' -> '{forbidden.To}' is present in graph")); } } // Strict mode: check for unexpected elements if (_oracle.StrictMode) { var unexpectedNodes = FindUnexpectedNodes(graph); foreach (var node in unexpectedNodes) { violations.Add(new PatchOracleViolation( ViolationType.UnexpectedFunction, node.Id, null, $"Strict mode: unexpected function '{node.Id}' found in graph")); } var unexpectedEdges = FindUnexpectedEdges(graph); foreach (var edge in unexpectedEdges) { violations.Add(new PatchOracleViolation( ViolationType.UnexpectedEdge, edge.From, edge.To, $"Strict mode: unexpected edge '{edge.From}' -> '{edge.To}' found in graph")); } } return new PatchOracleResult( OracleId: _oracle.Id, CaseRef: _oracle.CaseRef, Variant: _oracle.Variant, Success: violations.Count == 0, Violations: violations, Summary: GenerateSummary(graph, violations)); } private bool HasMatchingNode(RichGraph graph, ExpectedFunction expected) { foreach (var node in graph.Nodes) { if (!MatchesPattern(node.Id, expected.SymbolId) && !MatchesPattern(node.SymbolId, expected.SymbolId)) { continue; } if (!string.IsNullOrEmpty(expected.Lang) && !string.Equals(node.Lang, expected.Lang, StringComparison.OrdinalIgnoreCase)) { continue; } if (!string.IsNullOrEmpty(expected.Kind) && !string.Equals(node.Kind, expected.Kind, StringComparison.OrdinalIgnoreCase)) { continue; } if (!string.IsNullOrEmpty(expected.PurlPattern) && !MatchesPattern(node.Purl ?? string.Empty, expected.PurlPattern)) { continue; } return true; } return false; } private bool HasMatchingEdge(RichGraph graph, ExpectedEdge expected, double minConfidence) { foreach (var edge in graph.Edges) { if (!MatchesPattern(edge.From, expected.From)) { continue; } if (!MatchesPattern(edge.To, expected.To)) { continue; } if (!string.IsNullOrEmpty(expected.Kind) && !string.Equals(edge.Kind, expected.Kind, StringComparison.OrdinalIgnoreCase)) { continue; } if (edge.Confidence < minConfidence) { continue; } return true; } return false; } private bool HasMatchingRoot(RichGraph graph, ExpectedRoot expected) { foreach (var root in graph.Roots) { if (!MatchesPattern(root.Id, expected.Id)) { continue; } if (!string.IsNullOrEmpty(expected.Phase) && !string.Equals(root.Phase, expected.Phase, StringComparison.OrdinalIgnoreCase)) { continue; } return true; } return false; } private IEnumerable FindUnexpectedNodes(RichGraph graph) { var allExpected = _oracle.ExpectedFunctions .Select(f => f.SymbolId) .ToHashSet(StringComparer.Ordinal); foreach (var node in graph.Nodes) { var isExpected = allExpected.Any(pattern => MatchesPattern(node.Id, pattern) || MatchesPattern(node.SymbolId, pattern)); if (!isExpected) { yield return node; } } } private IEnumerable FindUnexpectedEdges(RichGraph graph) { foreach (var edge in graph.Edges) { var isExpected = _oracle.ExpectedEdges.Any(e => MatchesPattern(edge.From, e.From) && MatchesPattern(edge.To, e.To)); if (!isExpected) { yield return edge; } } } /// /// Matches a value against a pattern supporting '*' wildcards. /// private static bool MatchesPattern(string value, string pattern) { if (string.IsNullOrEmpty(pattern)) { return true; } if (string.IsNullOrEmpty(value)) { return false; } // Exact match if (!pattern.Contains('*')) { return string.Equals(value, pattern, StringComparison.Ordinal); } // Convert wildcard pattern to regex var regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$"; return Regex.IsMatch(value, regexPattern, RegexOptions.None, TimeSpan.FromMilliseconds(100)); } private static PatchOracleSummary GenerateSummary(RichGraph graph, List violations) { return new PatchOracleSummary( TotalNodes: graph.Nodes.Count, TotalEdges: graph.Edges.Count, TotalRoots: graph.Roots.Count, MissingFunctions: violations.Count(v => v.Type == ViolationType.MissingFunction), MissingEdges: violations.Count(v => v.Type == ViolationType.MissingEdge), MissingRoots: violations.Count(v => v.Type == ViolationType.MissingRoot), ForbiddenFunctionsPresent: violations.Count(v => v.Type == ViolationType.ForbiddenFunctionPresent), ForbiddenEdgesPresent: violations.Count(v => v.Type == ViolationType.ForbiddenEdgePresent), UnexpectedFunctions: violations.Count(v => v.Type == ViolationType.UnexpectedFunction), UnexpectedEdges: violations.Count(v => v.Type == ViolationType.UnexpectedEdge)); } } /// /// Result of comparing a graph against a patch-oracle. /// public sealed record PatchOracleResult( string OracleId, string CaseRef, string Variant, bool Success, IReadOnlyList Violations, PatchOracleSummary Summary) { /// /// Generates a human-readable report. /// public string ToReport() { var lines = new List { $"Patch-Oracle Validation Report", $"==============================", $"Oracle: {OracleId}", $"Case: {CaseRef} ({Variant})", $"Status: {(Success ? "PASS" : "FAIL")}", string.Empty, $"Graph Statistics:", $" Nodes: {Summary.TotalNodes}", $" Edges: {Summary.TotalEdges}", $" Roots: {Summary.TotalRoots}", string.Empty }; if (Violations.Count > 0) { lines.Add($"Violations ({Violations.Count}):"); foreach (var v in Violations) { var target = v.To is not null ? $" -> {v.To}" : string.Empty; lines.Add($" [{v.Type}] {v.From}{target}"); lines.Add($" Reason: {v.Message}"); } } else { lines.Add("No violations found."); } return string.Join(Environment.NewLine, lines); } } /// /// A single violation found during oracle comparison. /// public sealed record PatchOracleViolation( ViolationType Type, string From, string? To, string Message); /// /// Type of oracle violation. /// public enum ViolationType { MissingFunction, MissingEdge, MissingRoot, ForbiddenFunctionPresent, ForbiddenEdgePresent, UnexpectedFunction, UnexpectedEdge } /// /// Summary statistics for oracle comparison. /// public sealed record PatchOracleSummary( int TotalNodes, int TotalEdges, int TotalRoots, int MissingFunctions, int MissingEdges, int MissingRoots, int ForbiddenFunctionsPresent, int ForbiddenEdgesPresent, int UnexpectedFunctions, int UnexpectedEdges);