/** * NodeId - Content-Addressed Node Identifier * Sprint: SPRINT_9100_0001_0001 (Core Resolver Package) * Task: RESOLVER-9100-002 * * A content-addressed identifier for graph nodes. * NodeId = sha256(normalize(kind + ":" + key)) * * Guarantees: * - Same (kind, key) → same NodeId * - Different (kind, key) → different NodeId (collision resistant) * - Deterministic ordering via ordinal string comparison */ using System.Security.Cryptography; using System.Text; namespace StellaOps.Resolver; /// /// Content-addressed node identifier computed as SHA256 of normalized kind:key. /// Immutable value type for deterministic graph operations. /// public readonly record struct NodeId : IComparable, IEquatable { private readonly string _value; /// /// The SHA256 hex digest (lowercase, 64 characters). /// public string Value => _value ?? string.Empty; private NodeId(string value) => _value = value; /// /// Creates a NodeId from a pre-computed digest value. /// Use for computing from kind/key. /// /// A valid SHA256 hex digest (64 lowercase hex chars). public static NodeId FromDigest(string digest) { ArgumentException.ThrowIfNullOrWhiteSpace(digest); if (digest.Length != 64) throw new ArgumentException("NodeId digest must be 64 hex characters", nameof(digest)); return new NodeId(digest.ToLowerInvariant()); } /// /// Computes a NodeId from kind and key. /// Applies NFC normalization before hashing. /// /// Node kind (e.g., "package", "file", "symbol"). /// Node key (e.g., PURL, file path, symbol name). /// Content-addressed NodeId. public static NodeId From(string kind, string key) { ArgumentException.ThrowIfNullOrWhiteSpace(kind); ArgumentException.ThrowIfNullOrWhiteSpace(key); // NFC normalize inputs for Unicode consistency var normalizedKind = kind.Normalize(NormalizationForm.FormC); var normalizedKey = key.Normalize(NormalizationForm.FormC); var input = $"{normalizedKind}:{normalizedKey}"; var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); var digest = Convert.ToHexString(hash).ToLowerInvariant(); return new NodeId(digest); } /// /// Ordinal comparison for deterministic ordering. /// public int CompareTo(NodeId other) => string.Compare(Value, other.Value, StringComparison.Ordinal); /// /// Equality is based on digest value. /// public bool Equals(NodeId 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 <(NodeId left, NodeId right) => left.CompareTo(right) < 0; public static bool operator >(NodeId left, NodeId right) => left.CompareTo(right) > 0; public static bool operator <=(NodeId left, NodeId right) => left.CompareTo(right) <= 0; public static bool operator >=(NodeId left, NodeId right) => left.CompareTo(right) >= 0; }