/** * 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; /// /// Content-addressed edge identifier computed as SHA256 of src->kind->dst. /// Immutable value type for deterministic graph operations. /// public readonly record struct EdgeId : IComparable, IEquatable { private readonly string _value; /// /// The SHA256 hex digest (lowercase, 64 characters). /// public string Value => _value ?? string.Empty; private EdgeId(string value) => _value = value; /// /// Creates an EdgeId from a pre-computed digest value. /// Use for computing from components. /// /// A valid SHA256 hex digest (64 lowercase hex chars). 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()); } /// /// Computes an EdgeId from source, kind, and destination. /// Format: sha256(srcId->kind->dstId) /// /// Source node identifier. /// Edge kind (e.g., "depends_on", "calls", "imports"). /// Destination node identifier. /// Content-addressed EdgeId. 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); } /// /// Ordinal comparison for deterministic ordering. /// public int CompareTo(EdgeId other) => string.Compare(Value, other.Value, StringComparison.Ordinal); /// /// Equality is based on digest value. /// 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; }