Change-Trace Trust-Delta Formula Contract

Version: 1.0.0 Status: Draft Last Updated: 2026-01-12


Overview

This document specifies the mathematical formula and algorithm for computing trust-delta scores in StellaOps Change-Trace. The formula integrates VexLens consensus scores, reachability analysis, and patch verification to produce a deterministic risk assessment.

Audience: Scanner engineers implementing or auditing the trust-delta scorer, and reviewers reasoning about why a release earned a given verdict. For the wire schema of the trustDelta and summary objects this formula populates, see the Change-Trace Schema Contract.

The canonical implementation is TrustDeltaCalculator (src/Scanner/__Libraries/StellaOps.Scanner.ChangeTrace/Scoring/TrustDeltaCalculator.cs); weights and thresholds live in TrustDeltaOptions (same directory).


Core Formula

Trust Delta Calculation

TrustDelta = (BeforeTrust - AfterTrust) / max(BeforeTrust, 0.01)

Where:

Sign convention (this is the load-bearing detail). The numerator is Before − After, so:

This matches the implementation (delta = (beforeTrust - afterTrust) / Math.Max(beforeTrust, MinTrustDenominator)) and the verdict thresholds in the Verdict Mapping section below.

Before Trust Score

BeforeTrust = VexConsensus(purl, fromVersion) * ReachabilityFactor(fromReachablePaths)

After Trust Score

AfterTrust = VexConsensus(purl, toVersion) * ReachabilityFactor(toReachablePaths) + PatchBonus

Component Definitions

VEX Consensus Score

The VEX consensus score is obtained from VexLens, which aggregates trust assessments across multiple issuers.

VexConsensus(purl, version) -> [0.0, 1.0]
Score RangeInterpretation
0.9 - 1.0High trust (no known vulnerabilities)
0.7 - 0.9Moderate trust (minor issues)
0.5 - 0.7Low trust (some concerns)
0.0 - 0.5Very low trust (significant vulnerabilities)

Reachability Factor

The reachability factor adjusts trust based on whether vulnerable code is actually reachable.

ReachabilityFactor(callPaths) =
  - 1.0   if callPaths > 0      (reachable)
  - 0.7   if callPaths == 0     (unreachable, 30% reduction in impact)
  - 1.0   if callPaths is null  (unknown, assume reachable)

Rationale: Unreachable vulnerable code poses less risk, so we reduce the weight of the trust score. However, we don’t eliminate it entirely because reachability analysis may have gaps.

Patch Verification Bonus

The patch bonus rewards verified security patches with additional trust.

PatchBonus =
    FunctionMatchWeight * PatchVerificationConfidence
  + SectionMatchWeight * SymbolSimilarity
  + AttestationWeight * IssuerAuthorityScore (if DSSE present)

Default Weights

WeightValueDescription
FunctionMatchWeight0.25Weight for function-level match confidence
SectionMatchWeight0.15Weight for section-level similarity
AttestationWeight0.10Weight for DSSE attestation presence

Configurable via TrustDeltaOptions

public sealed record TrustDeltaOptions
{
    public double FunctionMatchWeight { get; init; } = 0.25;
    public double SectionMatchWeight { get; init; } = 0.15;
    public double AttestationWeight { get; init; } = 0.10;
    public double RuntimeConfirmWeight { get; init; } = 0.10;
    public double SignificantDeltaThreshold { get; init; } = 0.3;
}

Verdict Mapping

Trust Delta to Verdict

Verdict values are emitted in the camelCase wire form (riskDown / neutral / riskUp) by the canonical serializer; see the Change-Trace Schema Contract. The thresholds below match ChangeTraceBuilder.ComputeVerdict(riskDelta).

Delta RangeVerdictDescription
delta < -0.3riskDownSignificant risk reduction (trust rose).
-0.3 ≤ delta ≤ 0.3neutralNo significant change.
delta > 0.3riskUpSignificant risk increase (trust fell).

Reachability Impact

Before PathsAfter PathsImpact
nullanyunchanged
anynullunchanged
0> 0introduced
> 00eliminated
N< Nreduced
N> Nincreased
NNunchanged

Exploitability Impact

Delta RangeImpact
delta <= -0.5eliminated
-0.5 < delta < -0.1down
-0.1 <= delta <= 0.1unchanged
0.1 < delta < 0.5up
delta >= 0.5introduced

Worked Examples

Example 1: Security Patch Applied

Scenario: libssl3 updated from 3.0.9-1 to 3.0.9-1+deb12u3 (Debian security patch)

Inputs:

Calculation:

BeforeTrust = 0.45 * 1.0 = 0.45                      (reachable before)

PatchBonus = (0.25 * 0.97) + (0.15 * 0.85) + (0.10 * 0.90)
           = 0.2425 + 0.1275 + 0.09
           = 0.46

AfterTrust = 0.95 * 0.7 + 0.46 = 0.665 + 0.46 = 1.125
           -> Clamped to [0.0, 1.0] => 1.0 before the delta is computed

TrustDelta = (BeforeTrust - AfterTrust) / max(BeforeTrust, 0.01)
           = (0.45 - 1.0) / max(0.45, 0.01)
           = -0.55 / 0.45
           = -1.22
           -> Clamped to [-1.0, +1.0] => -1.0

Final: TrustDelta = -1.0 (clamped)
Verdict: riskDown (delta < -0.3)

Note: A negative delta means risk decreased — AfterTrust (1.0) exceeds BeforeTrust (0.45) after the verified patch, so trust rose. The verdict tracks the sign directly: riskDown for negative deltas.

Example 2: Version Upgrade with New Vulnerability

Scenario: openssl upgraded from 3.0.8 to 3.1.0 (new version introduces regression)

Inputs:

Calculation:

BeforeTrust = 0.85 * 0.7 = 0.595                     (unreachable before)

AfterTrust = 0.55 * 1.0 + 0 = 0.55                   (reachable after, no patch bonus)

TrustDelta = (BeforeTrust - AfterTrust) / max(BeforeTrust, 0.01)
           = (0.595 - 0.55) / max(0.595, 0.01)
           = 0.045 / 0.595
           = 0.076

Final: TrustDelta = 0.08
Verdict: neutral (within -0.3 to +0.3 range)

Analysis: The slightly positive delta reflects a small risk increase (the regression CVE is now reachable), but it stays within the neutral band because the before version also carried risk — just unreachable. The reachability change from 0 to 5 is captured in reachabilityImpact: introduced.

Example 3: Rebuild Without Changes

Scenario: Same source, different build timestamp

Inputs:

Calculation:

BeforeTrust = 0.90 * 1.0 = 0.90

AfterTrust = 0.90 * 1.0 + 0 = 0.90                   (no patch bonus for rebuild)

TrustDelta = (BeforeTrust - AfterTrust) / max(BeforeTrust, 0.01)
           = (0.90 - 0.90) / max(0.90, 0.01)
           = 0 / 0.90
           = 0

Final: TrustDelta = 0.00
Verdict: neutral

Proof Step Generation

Step Categories

  1. CVE Context: List CVEs affecting the package
  2. Version Change: Document version transition
  3. Patch Verification: Report verification method and confidence
  4. Symbol Similarity: Report code similarity metrics
  5. Reachability: Report call path changes
  6. Attestation: Note DSSE attestation presence
  7. Verdict: Final determination

Step Format

1. {CVE-ID} affects {function_name}
2. Version changed: {from} -> {to}
3. Patch verified via {method}: {confidence}% confidence
4. Symbol similarity: {percentage}%
5. Reachable call paths: {before} -> {after}
6. DSSE attestation present
7. Verdict: {verdict} ({delta:+0.00})

Example Output

1. CVE-2026-12345 affects ssl3_get_record
2. Version changed: 3.0.9-1 -> 3.0.9-1+deb12u3
3. Patch verified via CFG match: 97% confidence
4. Symbol similarity: 85%
5. Reachable call paths: 3 -> 0 after patch
6. DSSE attestation present
7. Verdict: riskDown (-0.27)

Algorithm Pseudocode

def calculate_trust_delta(context: TrustDeltaContext, options: TrustDeltaOptions) -> TrustDelta:
    # Get VEX consensus scores
    before_consensus = vexlens.get_consensus(context.purl, context.from_version)
    after_consensus = vexlens.get_consensus(context.purl, context.to_version)

    # Compute reachability factors
    before_reach = compute_reachability_factor(context.reachable_paths_before)
    after_reach = compute_reachability_factor(context.reachable_paths_after)

    # Compute base trust scores
    before_trust = before_consensus.trust_score * before_reach
    after_trust = after_consensus.trust_score * after_reach

    # Add patch verification bonus
    patch_bonus = 0.0
    if context.patch_verification_confidence:
        patch_bonus += options.function_match_weight * context.patch_verification_confidence
    if context.symbol_similarity:
        patch_bonus += options.section_match_weight * context.symbol_similarity
    if context.has_dsse_attestation and context.issuer_authority_score:
        patch_bonus += options.attestation_weight * context.issuer_authority_score

    after_trust += patch_bonus

    # Clamp trust scores to [0, 1] before computing the delta
    before_trust = clamp(before_trust, 0.0, 1.0)
    after_trust = clamp(after_trust, 0.0, 1.0)

    # Compute delta with division protection.
    # Numerator is (before - after): negative => risk down, positive => risk up.
    delta = (before_trust - after_trust) / max(before_trust, 0.01)
    delta = clamp(delta, -1.0, 1.0)

    # Determine impacts
    reachability_impact = determine_reachability_impact(
        context.reachable_paths_before,
        context.reachable_paths_after
    )
    exploitability_impact = determine_exploitability_impact(delta)

    # Generate proof steps
    proof_steps = generate_proof_steps(context, delta)

    return TrustDelta(
        score=round(delta, 2),
        before_score=round(before_trust, 2),
        after_score=round(after_trust, 2),
        reachability_impact=reachability_impact,
        exploitability_impact=exploitability_impact,
        proof_steps=proof_steps
    )


def compute_reachability_factor(call_paths: int | None) -> float:
    if call_paths is None:
        return 1.0  # Unknown, assume reachable
    if call_paths == 0:
        return 0.7  # Unreachable, 30% reduction
    return 1.0  # Reachable


def determine_reachability_impact(before: int | None, after: int | None) -> ReachabilityImpact:
    if before is None or after is None:
        return ReachabilityImpact.UNCHANGED
    if before == 0 and after > 0:
        return ReachabilityImpact.INTRODUCED
    if before > 0 and after == 0:
        return ReachabilityImpact.ELIMINATED
    if after < before:
        return ReachabilityImpact.REDUCED
    if after > before:
        return ReachabilityImpact.INCREASED
    return ReachabilityImpact.UNCHANGED


def determine_exploitability_impact(delta: float) -> ExploitabilityImpact:
    if delta <= -0.5:
        return ExploitabilityImpact.ELIMINATED
    if delta < -0.1:
        return ExploitabilityImpact.DOWN
    if delta > 0.5:
        return ExploitabilityImpact.INTRODUCED
    if delta > 0.1:
        return ExploitabilityImpact.UP
    return ExploitabilityImpact.UNCHANGED

Determinism Requirements

Reproducibility

Given identical inputs, the formula must produce identical outputs:

Floating Point Handling

Timestamp Independence

The formula does not depend on current time. All inputs are point-in-time snapshots.


Versioning

Algorithm Version

The formula version is tied to this contract (1.0.0). In a Change-Trace artifact, the producing engine is identified by basis.engineVersion (and optionally basis.engineDigest) per the Change-Trace Schema Contract — there is no separate top-level algorithmVersion field in the artifact model.

Backward Compatibility


References