up
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Ordering;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical (deterministically ordered) graph representation.
|
||||
/// </summary>
|
||||
public sealed class CanonicalGraph
|
||||
{
|
||||
/// <summary>
|
||||
/// Ordering strategy used.
|
||||
/// </summary>
|
||||
public required GraphOrderingStrategy Strategy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Deterministically ordered nodes.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<CanonicalNode> Nodes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Deterministically ordered edges.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<CanonicalEdge> Edges { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content hash of canonical representation.
|
||||
/// </summary>
|
||||
public required string ContentHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Anchor nodes (entry points), if present.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? AnchorNodes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional timestamp for diagnostics; excluded from <see cref="ContentHash"/>.
|
||||
/// </summary>
|
||||
public DateTimeOffset? ComputedAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CanonicalNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Position in ordered list (0-indexed).
|
||||
/// </summary>
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Node identifier.
|
||||
/// </summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Node type (e.g. method, function).
|
||||
/// </summary>
|
||||
public required string NodeType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Node label for UI display (optional).
|
||||
/// </summary>
|
||||
public string? Label { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CanonicalEdge
|
||||
{
|
||||
public required int SourceIndex { get; init; }
|
||||
public required int TargetIndex { get; init; }
|
||||
public required string EdgeType { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Ordering;
|
||||
|
||||
public sealed class DeterministicGraphOrderer : IGraphOrderer
|
||||
{
|
||||
public IReadOnlyList<string> OrderNodes(
|
||||
RichGraph graph,
|
||||
GraphOrderingStrategy strategy = GraphOrderingStrategy.TopologicalLexicographic)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(graph);
|
||||
|
||||
return strategy switch
|
||||
{
|
||||
GraphOrderingStrategy.TopologicalLexicographic => TopologicalLexicographicOrder(graph),
|
||||
GraphOrderingStrategy.BreadthFirstLexicographic => BreadthFirstLexicographicOrder(graph),
|
||||
GraphOrderingStrategy.DepthFirstLexicographic => DepthFirstLexicographicOrder(graph),
|
||||
GraphOrderingStrategy.Lexicographic => LexicographicOrder(graph),
|
||||
_ => TopologicalLexicographicOrder(graph)
|
||||
};
|
||||
}
|
||||
|
||||
public IReadOnlyList<RichGraphEdge> OrderEdges(
|
||||
RichGraph graph,
|
||||
IReadOnlyList<string> nodeOrder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(graph);
|
||||
ArgumentNullException.ThrowIfNull(nodeOrder);
|
||||
|
||||
var index = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < nodeOrder.Count; i++)
|
||||
{
|
||||
index[nodeOrder[i]] = i;
|
||||
}
|
||||
|
||||
return graph.Edges
|
||||
.Where(e => index.ContainsKey(e.From) && index.ContainsKey(e.To))
|
||||
.OrderBy(e => index[e.From])
|
||||
.ThenBy(e => index[e.To])
|
||||
.ThenBy(e => e.Kind, StringComparer.Ordinal)
|
||||
.ThenBy(e => e.Purl, StringComparer.Ordinal)
|
||||
.ThenBy(e => e.SymbolDigest, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public CanonicalGraph Canonicalize(
|
||||
RichGraph graph,
|
||||
GraphOrderingStrategy strategy = GraphOrderingStrategy.TopologicalLexicographic)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(graph);
|
||||
|
||||
var nodeById = graph.Nodes
|
||||
.Where(n => !string.IsNullOrWhiteSpace(n.Id))
|
||||
.GroupBy(n => n.Id, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
|
||||
|
||||
var nodeOrder = OrderNodes(graph, strategy);
|
||||
var edges = OrderEdges(graph, nodeOrder);
|
||||
|
||||
var index = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < nodeOrder.Count; i++)
|
||||
{
|
||||
index[nodeOrder[i]] = i;
|
||||
}
|
||||
|
||||
var nodes = nodeOrder
|
||||
.Select(id =>
|
||||
{
|
||||
nodeById.TryGetValue(id, out var node);
|
||||
return new CanonicalNode
|
||||
{
|
||||
Index = index[id],
|
||||
Id = id,
|
||||
NodeType = node?.Kind ?? "unknown",
|
||||
Label = node?.Display
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var canonicalEdges = edges
|
||||
.Select(e => new CanonicalEdge
|
||||
{
|
||||
SourceIndex = index[e.From],
|
||||
TargetIndex = index[e.To],
|
||||
EdgeType = e.Kind
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var anchors = FindAnchorNodes(graph, nodeOrder);
|
||||
|
||||
return new CanonicalGraph
|
||||
{
|
||||
Strategy = strategy,
|
||||
Nodes = nodes,
|
||||
Edges = canonicalEdges,
|
||||
ContentHash = ComputeCanonicalHash(nodes, canonicalEdges),
|
||||
AnchorNodes = anchors
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> TopologicalLexicographicOrder(RichGraph graph)
|
||||
{
|
||||
var nodes = graph.Nodes.Select(n => n.Id).Where(id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.Ordinal).ToList();
|
||||
nodes.Sort(StringComparer.Ordinal);
|
||||
|
||||
var adjacency = nodes.ToDictionary(n => n, _ => new List<string>(), StringComparer.Ordinal);
|
||||
var indegree = nodes.ToDictionary(n => n, _ => 0, StringComparer.Ordinal);
|
||||
|
||||
foreach (var edge in graph.Edges)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(edge.From) || string.IsNullOrWhiteSpace(edge.To))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!adjacency.TryGetValue(edge.From, out var neighbors) || !indegree.ContainsKey(edge.To))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
neighbors.Add(edge.To);
|
||||
indegree[edge.To] = indegree[edge.To] + 1;
|
||||
}
|
||||
|
||||
foreach (var list in adjacency.Values)
|
||||
{
|
||||
list.Sort(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
var ready = new SortedSet<string>(indegree.Where(kv => kv.Value == 0).Select(kv => kv.Key), StringComparer.Ordinal);
|
||||
var result = new List<string>(nodes.Count);
|
||||
|
||||
while (ready.Count > 0)
|
||||
{
|
||||
var next = ready.Min!;
|
||||
ready.Remove(next);
|
||||
result.Add(next);
|
||||
|
||||
foreach (var neighbor in adjacency[next])
|
||||
{
|
||||
indegree[neighbor] = indegree[neighbor] - 1;
|
||||
if (indegree[neighbor] == 0)
|
||||
{
|
||||
ready.Add(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Count == nodes.Count)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(result, StringComparer.Ordinal);
|
||||
var remainder = nodes
|
||||
.Where(n => !seen.Contains(n))
|
||||
.OrderBy(n => n, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
result.AddRange(remainder);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> BreadthFirstLexicographicOrder(RichGraph graph)
|
||||
{
|
||||
var ordered = new List<string>();
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
var adjacency = BuildAdjacency(graph);
|
||||
var entries = FindEntryPoints(graph).OrderBy(e => e, StringComparer.Ordinal).ToList();
|
||||
var queue = new Queue<string>(entries);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
if (!visited.Add(current))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ordered.Add(current);
|
||||
|
||||
if (!adjacency.TryGetValue(current, out var neighbors))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var neighbor in neighbors)
|
||||
{
|
||||
if (!visited.Contains(neighbor))
|
||||
{
|
||||
queue.Enqueue(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append disconnected nodes deterministically
|
||||
foreach (var nodeId in graph.Nodes.Select(n => n.Id).Distinct(StringComparer.Ordinal).OrderBy(n => n, StringComparer.Ordinal))
|
||||
{
|
||||
if (visited.Add(nodeId))
|
||||
{
|
||||
ordered.Add(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> DepthFirstLexicographicOrder(RichGraph graph)
|
||||
{
|
||||
var ordered = new List<string>();
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
var adjacency = BuildAdjacency(graph);
|
||||
var entries = FindEntryPoints(graph).OrderBy(e => e, StringComparer.Ordinal).ToList();
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
DfsVisit(entry, adjacency, visited, ordered);
|
||||
}
|
||||
|
||||
// Append disconnected nodes deterministically
|
||||
foreach (var nodeId in graph.Nodes.Select(n => n.Id).Distinct(StringComparer.Ordinal).OrderBy(n => n, StringComparer.Ordinal))
|
||||
{
|
||||
DfsVisit(nodeId, adjacency, visited, ordered);
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private static void DfsVisit(
|
||||
string node,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>> adjacency,
|
||||
HashSet<string> visited,
|
||||
List<string> result)
|
||||
{
|
||||
if (!visited.Add(node))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
result.Add(node);
|
||||
|
||||
if (!adjacency.TryGetValue(node, out var neighbors))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var neighbor in neighbors)
|
||||
{
|
||||
DfsVisit(neighbor, adjacency, visited, result);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> LexicographicOrder(RichGraph graph)
|
||||
{
|
||||
return graph.Nodes
|
||||
.Select(n => n.Id)
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, IReadOnlyList<string>> BuildAdjacency(RichGraph graph)
|
||||
{
|
||||
var adjacency = new Dictionary<string, List<string>>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var edge in graph.Edges)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(edge.From) || string.IsNullOrWhiteSpace(edge.To))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!adjacency.TryGetValue(edge.From, out var list))
|
||||
{
|
||||
list = new List<string>();
|
||||
adjacency[edge.From] = list;
|
||||
}
|
||||
|
||||
list.Add(edge.To);
|
||||
}
|
||||
|
||||
// Deterministic neighbor traversal
|
||||
foreach (var list in adjacency.Values)
|
||||
{
|
||||
list.Sort(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
return adjacency.ToDictionary(kv => kv.Key, kv => (IReadOnlyList<string>)kv.Value, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> FindEntryPoints(RichGraph graph)
|
||||
{
|
||||
var nodeIds = graph.Nodes.Select(n => n.Id).Where(id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.Ordinal).ToList();
|
||||
var inbound = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var edge in graph.Edges)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(edge.To))
|
||||
{
|
||||
inbound.Add(edge.To);
|
||||
}
|
||||
}
|
||||
|
||||
var roots = (graph.Roots ?? Array.Empty<RichGraphRoot>())
|
||||
.Select(r => r.Id)
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.ToList();
|
||||
|
||||
var semanticEntrypoints = graph.Nodes
|
||||
.Where(IsEntrypointNode)
|
||||
.Select(n => n.Id)
|
||||
.ToList();
|
||||
|
||||
var entryPoints = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var root in roots)
|
||||
{
|
||||
entryPoints.Add(root);
|
||||
}
|
||||
|
||||
foreach (var entry in semanticEntrypoints)
|
||||
{
|
||||
entryPoints.Add(entry);
|
||||
}
|
||||
|
||||
foreach (var nodeId in nodeIds)
|
||||
{
|
||||
if (!inbound.Contains(nodeId))
|
||||
{
|
||||
entryPoints.Add(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return entryPoints.OrderBy(id => id, StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string>? FindAnchorNodes(RichGraph graph, IReadOnlyList<string> nodeOrder)
|
||||
{
|
||||
var anchors = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var root in graph.Roots ?? Array.Empty<RichGraphRoot>())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(root.Id))
|
||||
{
|
||||
anchors.Add(root.Id);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var node in graph.Nodes)
|
||||
{
|
||||
if (IsEntrypointNode(node))
|
||||
{
|
||||
anchors.Add(node.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (anchors.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return nodeOrder.Where(anchors.Contains).ToList();
|
||||
}
|
||||
|
||||
private static string ComputeCanonicalHash(
|
||||
IReadOnlyList<CanonicalNode> nodes,
|
||||
IReadOnlyList<CanonicalEdge> edges)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
sb.Append("N:");
|
||||
sb.Append(node.Index);
|
||||
sb.Append(':');
|
||||
sb.Append(node.Id);
|
||||
sb.Append(':');
|
||||
sb.Append(node.NodeType);
|
||||
sb.Append(';');
|
||||
}
|
||||
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
sb.Append("E:");
|
||||
sb.Append(edge.SourceIndex);
|
||||
sb.Append(':');
|
||||
sb.Append(edge.TargetIndex);
|
||||
sb.Append(':');
|
||||
sb.Append(edge.EdgeType);
|
||||
sb.Append(';');
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static bool IsEntrypointNode(RichGraphNode node)
|
||||
{
|
||||
if (node.Attributes?.TryGetValue(RichGraphSemanticAttributes.IsEntrypoint, out var value) != true ||
|
||||
string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return bool.TryParse(value, out var result) && result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace StellaOps.Scanner.Reachability.Ordering;
|
||||
|
||||
/// <summary>
|
||||
/// Strategy for deterministic graph node ordering.
|
||||
/// </summary>
|
||||
public enum GraphOrderingStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Topological sort with lexicographic tiebreaker.
|
||||
/// Best for DAGs (call graphs).
|
||||
/// </summary>
|
||||
TopologicalLexicographic,
|
||||
|
||||
/// <summary>
|
||||
/// Breadth-first from entry points with lexicographic tiebreaker.
|
||||
/// Best for displaying reachability paths.
|
||||
/// </summary>
|
||||
BreadthFirstLexicographic,
|
||||
|
||||
/// <summary>
|
||||
/// Depth-first from entry points with lexicographic tiebreaker.
|
||||
/// Best for call stack visualization.
|
||||
/// </summary>
|
||||
DepthFirstLexicographic,
|
||||
|
||||
/// <summary>
|
||||
/// Pure lexicographic ordering by node ID.
|
||||
/// Most predictable, may not respect graph structure.
|
||||
/// </summary>
|
||||
Lexicographic
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Ordering;
|
||||
|
||||
/// <summary>
|
||||
/// Orders graph nodes deterministically.
|
||||
/// </summary>
|
||||
public interface IGraphOrderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Orders nodes in the graph deterministically.
|
||||
/// </summary>
|
||||
/// <param name="graph">The graph to order.</param>
|
||||
/// <param name="strategy">Ordering strategy to use.</param>
|
||||
/// <returns>Ordered list of node IDs.</returns>
|
||||
IReadOnlyList<string> OrderNodes(
|
||||
RichGraph graph,
|
||||
GraphOrderingStrategy strategy = GraphOrderingStrategy.TopologicalLexicographic);
|
||||
|
||||
/// <summary>
|
||||
/// Orders edges deterministically based on node ordering.
|
||||
/// </summary>
|
||||
IReadOnlyList<RichGraphEdge> OrderEdges(
|
||||
RichGraph graph,
|
||||
IReadOnlyList<string> nodeOrder);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a canonical representation of the graph.
|
||||
/// </summary>
|
||||
CanonicalGraph Canonicalize(
|
||||
RichGraph graph,
|
||||
GraphOrderingStrategy strategy = GraphOrderingStrategy.TopologicalLexicographic);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user