93 lines
3.3 KiB
C#
93 lines
3.3 KiB
C#
/**
|
|
* EdgeId - Content-Addressed Edge Identifier
|
|
* Sprint: SPRINT_9100_0001_0003 (Content-Addressed EdgeId)
|
|
* Task: EDGEID-9100-001, EDGEID-9100-002, EDGEID-9100-003
|
|
*
|
|
* A content-addressed identifier for graph edges.
|
|
* EdgeId = sha256(srcId + "->" + kind + "->" + dstId)
|
|
*
|
|
* Enables:
|
|
* - Edge-level attestations
|
|
* - Delta detection between graphs
|
|
* - Merkle tree inclusion for proof chains
|
|
*/
|
|
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace StellaOps.Resolver;
|
|
|
|
/// <summary>
|
|
/// Content-addressed edge identifier computed as SHA256 of src->kind->dst.
|
|
/// Immutable value type for deterministic graph operations.
|
|
/// </summary>
|
|
public readonly record struct EdgeId : IComparable<EdgeId>, IEquatable<EdgeId>
|
|
{
|
|
private readonly string _value;
|
|
|
|
/// <summary>
|
|
/// The SHA256 hex digest (lowercase, 64 characters).
|
|
/// </summary>
|
|
public string Value => _value ?? string.Empty;
|
|
|
|
private EdgeId(string value) => _value = value;
|
|
|
|
/// <summary>
|
|
/// Creates an EdgeId from a pre-computed digest value.
|
|
/// Use <see cref="From(NodeId, string, NodeId)"/> for computing from components.
|
|
/// </summary>
|
|
/// <param name="digest">A valid SHA256 hex digest (64 lowercase hex chars).</param>
|
|
public static EdgeId FromDigest(string digest)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(digest);
|
|
if (digest.Length != 64)
|
|
throw new ArgumentException("EdgeId digest must be 64 hex characters", nameof(digest));
|
|
|
|
return new EdgeId(digest.ToLowerInvariant());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes an EdgeId from source, kind, and destination.
|
|
/// Format: sha256(srcId->kind->dstId)
|
|
/// </summary>
|
|
/// <param name="src">Source node identifier.</param>
|
|
/// <param name="kind">Edge kind (e.g., "depends_on", "calls", "imports").</param>
|
|
/// <param name="dst">Destination node identifier.</param>
|
|
/// <returns>Content-addressed EdgeId.</returns>
|
|
public static EdgeId From(NodeId src, string kind, NodeId dst)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(kind);
|
|
|
|
// NFC normalize kind for Unicode consistency
|
|
var normalizedKind = kind.Normalize(NormalizationForm.FormC);
|
|
|
|
var input = $"{src.Value}->{normalizedKind}->{dst.Value}";
|
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
|
var digest = Convert.ToHexString(hash).ToLowerInvariant();
|
|
|
|
return new EdgeId(digest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ordinal comparison for deterministic ordering.
|
|
/// </summary>
|
|
public int CompareTo(EdgeId other)
|
|
=> string.Compare(Value, other.Value, StringComparison.Ordinal);
|
|
|
|
/// <summary>
|
|
/// Equality is based on digest value.
|
|
/// </summary>
|
|
public bool Equals(EdgeId other)
|
|
=> string.Equals(Value, other.Value, StringComparison.Ordinal);
|
|
|
|
public override int GetHashCode()
|
|
=> Value.GetHashCode(StringComparison.Ordinal);
|
|
|
|
public override string ToString() => Value;
|
|
|
|
public static bool operator <(EdgeId left, EdgeId right) => left.CompareTo(right) < 0;
|
|
public static bool operator >(EdgeId left, EdgeId right) => left.CompareTo(right) > 0;
|
|
public static bool operator <=(EdgeId left, EdgeId right) => left.CompareTo(right) <= 0;
|
|
public static bool operator >=(EdgeId left, EdgeId right) => left.CompareTo(right) >= 0;
|
|
}
|