# Product Advisory: Deterministic Resolver Architecture **Date:** 2025-12-24 **Author:** Architecture Guild **Status:** Approved for Implementation **Sprint Epoch:** 9100 **Priority:** P0 (Critical Path for Audit Compliance) --- ## Executive Summary This advisory defines the architecture for a **unified deterministic resolver** that evaluates vulnerability graphs using only declared evidence edges and produces cryptographically verifiable, reproducible verdicts. The resolver guarantees: **same inputs → same traversal order → same per-node inputs → same verdicts → same output digest**. This is a foundational capability for: - Auditor-friendly verification ("same inputs → same verdicts") - Offline replay by vendors/distributors - Single-digest comparison across runs - CI/CD gate assertions --- ## Problem Statement Current implementation has strong building blocks but lacks unified orchestration: | Component | Location | Gap | |-----------|----------|-----| | Topological Sort | `DeterministicGraphOrderer` | Traversal only; no verdict output | | Lattice Evaluation | `TrustLatticeEngine`, `PolicyEvaluator` | No traversal sequence in output | | Content Addressing | `ContentAddressedIdGenerator` | Node IDs only; no edge IDs | | Purity Analysis | `ProhibitedPatternAnalyzer` | Static only; no runtime enforcement | | Digest Computation | Various | No unified "run digest" | **Result:** Cannot produce a single `ResolutionResult` that captures: 1. The exact traversal sequence used 2. Per-node verdicts with individual digests 3. A composite `FinalDigest` for verification --- ## Solution Architecture ### Core Data Model ```csharp // New: src/__Libraries/StellaOps.Resolver/ /// /// Content-addressed node identifier. /// Format: sha256(kind || ":" || normalized-key) /// public sealed record NodeId(string Value) : IComparable { public int CompareTo(NodeId? other) => string.Compare(Value, other?.Value, StringComparison.Ordinal); public static NodeId From(string kind, string normalizedKey) => new(HexSha256($"{kind}:{normalizedKey}")); } /// /// Content-addressed edge identifier. /// Format: sha256(srcID || "->" || edgeKind || "->" || dstID) /// public sealed record EdgeId(string Value) : IComparable { public static EdgeId From(NodeId src, string kind, NodeId dst) => new(HexSha256($"{src.Value}->{kind}->{dst.Value}")); } /// /// Graph edge with content-addressed identity and optional cycle-cut marker. /// public sealed record Edge( EdgeId Id, NodeId Src, string Kind, NodeId Dst, JsonElement Attrs, bool IsCycleCut = false); /// /// Graph node with content-addressed identity. /// public sealed record Node( NodeId Id, string Kind, JsonElement Attrs); /// /// Immutable policy with content-addressed identity. /// public sealed record Policy( string Version, JsonElement Rules, string ConstantsDigest); /// /// Individual verdict with its own content-addressed digest. /// public sealed record Verdict( NodeId Node, string Status, JsonElement Evidence, string VerdictDigest); /// /// Complete resolution result with all digests for verification. /// public sealed record ResolutionResult( ImmutableArray TraversalSequence, ImmutableArray Verdicts, string GraphDigest, string PolicyDigest, string FinalDigest); ``` ### Resolver Algorithm ```text normalize(graph); // IDs, fields, ordering validate(graph); // No implicit data, cycles declared assert no-ambient-inputs(); // Runtime purity check order = topoSortWithLexTieBreak(graph); // Uses only evidence edges verdicts = [] for node in order: inbound = gatherInboundEvidence(node) // Pure, no IO verdict = Evaluate(node, inbound, policy) // Pure, no IO verdicts.append(canonicalize(verdict) with VerdictDigest) result = { traversal: order, verdicts: verdicts, graphDigest: digest(canonical(graph)), policyDigest: digest(canonical(policy)) } result.finalDigest = digest(canonical(result)) return result ``` ### Key Invariants 1. **Canonical IDs**: `NodeId = sha256(kind:normalized-key)`, `EdgeId = sha256(src->kind->dst)` 2. **Canonical Serialization**: Alphabetical keys, sorted arrays, fixed numerics, ISO-8601 Z timestamps 3. **Fixed Traversal**: Kahn's algorithm with lexicographic tie-breaker (`SortedSet`) 4. **Explicit Cycles**: Cycles require `IsCycleCut = true` edge; unmarked cycles → invalid graph 5. **Evidence-Only Evaluation**: Node verdict = f(node, inbound_edges, policy) — no ambient IO 6. **Stable Outputs**: `(TraversalSequence, Verdicts[], GraphDigest, PolicyDigest, FinalDigest)` --- ## Gap Analysis vs Current Implementation ### Already Implemented (No Work Needed) | Requirement | Implementation | Location | |-------------|---------------|----------| | Canonical JSON | RFC 8785 compliant | `CanonicalJsonSerializer`, `Rfc8785JsonCanonicalizer` | | Alphabetical key sorting | `StableDictionaryConverter` | `StellaOps.Canonicalization` | | ISO-8601 UTC timestamps | `Iso8601DateTimeConverter` | `StellaOps.Canonicalization` | | Topological sort with lex tie-break | `SortedSet` | `DeterministicGraphOrderer:134` | | K4 Four-Valued Lattice | Complete | `K4Lattice.cs` | | VEX Lattice Merging | With traces | `OpenVexStatementMerger` | | Merkle Tree Proofs | SHA256-based | `DeterministicMerkleTreeBuilder` | | Static Purity Analysis | 16+ patterns | `ProhibitedPatternAnalyzer` | | Injected Timestamp | Context-based | `PolicyEvaluationContext.Now` | | Graph Content Hash | SHA256 | `DeterministicGraphOrderer.ComputeCanonicalHash()` | ### Gaps to Implement | Gap | Priority | Sprint | Description | |-----|----------|--------|-------------| | Unified Resolver | P0 | 9100.0001.0001 | Single entry point producing `ResolutionResult` | | Cycle-Cut Edges | P1 | 9100.0001.0002 | `IsCycleCut` edge property; validation | | EdgeId | P2 | 9100.0001.0003 | Content-addressed edge identifiers | | FinalDigest | P1 | 9100.0002.0001 | Composite run-level digest | | Per-Node VerdictDigest | P2 | 9100.0002.0002 | Individual verdict digests | | Runtime Purity | P1 | 9100.0003.0001 | Runtime enforcement beyond static analysis | | Graph Validation + NFC | P3 | 9100.0003.0002 | Pre-traversal validation; NFC normalization | --- ## Implementation Phases ### Phase 1: Core Resolver Package (Sprint 9100.0001.*) **Goal:** Create `StellaOps.Resolver` library with unified resolver pattern. 1. **Sprint 9100.0001.0001** — Core Resolver - `DeterministicResolver` class - `ResolutionResult` record - Integration with existing `DeterministicGraphOrderer` - Integration with existing `TrustLatticeEngine` 2. **Sprint 9100.0001.0002** — Cycle-Cut Edges - `IsCycleCut` property on edges - Cycle validation (unmarked cycles → error) - Graph validation before traversal 3. **Sprint 9100.0001.0003** — Content-Addressed EdgeId - `EdgeId` record - Edge content addressing - Merkle tree inclusion of edges ### Phase 2: Digest Infrastructure (Sprint 9100.0002.*) **Goal:** Implement comprehensive digest chain for verification. 1. **Sprint 9100.0002.0001** — FinalDigest - Composite digest computation - `sha256(canonical({graphDigest, policyDigest, verdicts[]}))` - Integration with attestation system 2. **Sprint 9100.0002.0002** — Per-Node VerdictDigest - Verdict-level content addressing - Drill-down debugging ("which node changed?") - Delta detection between runs ### Phase 3: Purity & Validation (Sprint 9100.0003.*) **Goal:** Harden determinism guarantees with runtime enforcement. 1. **Sprint 9100.0003.0001** — Runtime Purity Enforcement - Runtime guards beyond static analysis - Dependency injection shims for ambient services - Test harness for purity verification 2. **Sprint 9100.0003.0002** — Graph Validation & NFC - Pre-traversal validation ("no implicit data") - Unicode NFC normalization for string fields - Evidence completeness assertions --- ## Testing Requirements ### Mandatory Test Types 1. **Replay Test**: Same input twice → identical `FinalDigest` 2. **Permutation Test**: Shuffle input nodes/edges → identical outputs 3. **Cycle Test**: Introduce cycle → fail unless `IsCycleCut` edge present 4. **Ambience Test**: Forbid calls to time/env/network during evaluation 5. **Serialization Test**: Canonicalization changes → predictable digest changes ### Property-Based Tests ```csharp // Idempotency [Property] public void Resolver_SameInput_SameOutput(Graph graph) { var result1 = resolver.Run(graph); var result2 = resolver.Run(graph); Assert.Equal(result1.FinalDigest, result2.FinalDigest); } // Order Independence [Property] public void Resolver_ShuffledInputs_SameOutput(Graph graph, int seed) { var shuffled = ShuffleNodesAndEdges(graph, seed); var result1 = resolver.Run(graph); var result2 = resolver.Run(shuffled); Assert.Equal(result1.FinalDigest, result2.FinalDigest); } // Cycle Detection [Property] public void Resolver_UnmarkedCycle_Throws(Graph graphWithCycle) { Assume.That(!graphWithCycle.Edges.Any(e => e.IsCycleCut)); Assert.Throws(() => resolver.Run(graphWithCycle)); } ``` --- ## Integration Points ### Existing Components to Integrate | Component | Integration | |-----------|-------------| | `DeterministicGraphOrderer` | Use for traversal order computation | | `TrustLatticeEngine` | Use for K4 lattice evaluation | | `CanonicalJsonSerializer` | Use for all serialization | | `ContentAddressedIdGenerator` | Extend for EdgeId and VerdictId | | `ProhibitedPatternAnalyzer` | Combine with runtime guards | | `PolicyEvaluationContext` | Use for injected inputs | ### New Modules | Module | Purpose | |--------|---------| | `StellaOps.Resolver` | Core resolver library | | `StellaOps.Resolver.Testing` | Test fixtures and harnesses | | `StellaOps.Resolver.Attestation` | Integration with proof chain | --- ## Success Criteria 1. **Unified API**: Single `resolver.Run(graph)` produces complete `ResolutionResult` 2. **Deterministic**: 100% pass rate on replay/permutation/cycle tests 3. **Auditable**: `FinalDigest` enables single-value verification 4. **Performant**: No more than 10% overhead vs current fragmented approach 5. **Documented**: Full API documentation and integration guide --- ## Appendix: C# Reference Implementation ```csharp public sealed class DeterministicResolver { private readonly Policy _policy; private readonly string _policyDigest; private readonly DeterministicGraphOrderer _orderer; private readonly TrustLatticeEngine _lattice; private readonly CanonicalJsonSerializer _serializer; public DeterministicResolver( Policy policy, DeterministicGraphOrderer orderer, TrustLatticeEngine lattice, CanonicalJsonSerializer serializer) { _policy = Canon(policy); _policyDigest = HexSha256(serializer.SerializeToBytes(_policy)); _orderer = orderer; _lattice = lattice; _serializer = serializer; } public ResolutionResult Run(EvidenceGraph graph) { // 1. Canonicalize and validate var canonical = _orderer.Canonicalize(graph); ValidateCycles(canonical); EnsureNoAmbientInputs(); // 2. Traverse in deterministic order var traversal = canonical.Nodes .Select(n => NodeId.Parse(n.Id)) .ToImmutableArray(); // 3. Evaluate each node purely var verdicts = new List(traversal.Length); foreach (var nodeId in traversal) { var node = GetNode(canonical, nodeId); var inbound = GatherInboundEvidence(canonical, nodeId); var verdict = EvaluatePure(node, inbound, _policy); var verdictBytes = _serializer.SerializeToBytes(verdict); verdicts.Add(verdict with { VerdictDigest = HexSha256(verdictBytes) }); } // 4. Compute digests var verdictsArray = verdicts.ToImmutableArray(); var resultWithoutFinal = new ResolutionResult( traversal, verdictsArray, canonical.ContentHash, _policyDigest, ""); var finalBytes = _serializer.SerializeToBytes(resultWithoutFinal); return resultWithoutFinal with { FinalDigest = HexSha256(finalBytes) }; } private void ValidateCycles(CanonicalGraph graph) { // Detect cycles; require IsCycleCut for each var cycles = DetectCycles(graph); var unmarked = cycles.Where(c => !c.CutEdge.IsCycleCut).ToList(); if (unmarked.Any()) { throw new InvalidGraphException( $"Graph contains {unmarked.Count} unmarked cycle(s). " + "Add IsCycleCut=true to break cycles."); } } private Verdict EvaluatePure(Node node, InboundEvidence evidence, Policy policy) { // Pure evaluation: no IO, no ambient state return _lattice.Evaluate(node, evidence, policy); } } ``` --- ## Related Documents - `docs/modules/attestor/proof-chain-specification.md` - `docs/modules/policy/architecture.md` - `docs/testing/testing-strategy-models.md` - `src/Policy/__Libraries/StellaOps.Policy/TrustLattice/K4Lattice.cs` - `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Ordering/DeterministicGraphOrderer.cs` --- ## Approval | Role | Name | Date | Decision | |------|------|------|----------| | Architecture Lead | — | 2025-12-24 | Approved | | Policy Guild Lead | — | 2025-12-24 | Approved | | Scanner Guild Lead | — | 2025-12-24 | Approved |