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);