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:
TrustDeltais clamped to[-1.0, +1.0].BeforeTrustandAfterTrustare computed per the sections below, each clamped to[0.0, 1.0].
Sign convention (this is the load-bearing detail). The numerator is Before − After, so:
- a negative delta means trust rose / risk decreased (the good case);
- a positive delta means trust fell / risk increased.
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 Range | Interpretation |
|---|---|
| 0.9 - 1.0 | High trust (no known vulnerabilities) |
| 0.7 - 0.9 | Moderate trust (minor issues) |
| 0.5 - 0.7 | Low trust (some concerns) |
| 0.0 - 0.5 | Very 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
| Weight | Value | Description |
|---|---|---|
FunctionMatchWeight | 0.25 | Weight for function-level match confidence |
SectionMatchWeight | 0.15 | Weight for section-level similarity |
AttestationWeight | 0.10 | Weight 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 Range | Verdict | Description |
|---|---|---|
delta < -0.3 | riskDown | Significant risk reduction (trust rose). |
-0.3 ≤ delta ≤ 0.3 | neutral | No significant change. |
delta > 0.3 | riskUp | Significant risk increase (trust fell). |
Reachability Impact
| Before Paths | After Paths | Impact |
|---|---|---|
| null | any | unchanged |
| any | null | unchanged |
| 0 | > 0 | introduced |
| > 0 | 0 | eliminated |
| N | < N | reduced |
| N | > N | increased |
| N | N | unchanged |
Exploitability Impact
| Delta Range | Impact |
|---|---|
delta <= -0.5 | eliminated |
-0.5 < delta < -0.1 | down |
-0.1 <= delta <= 0.1 | unchanged |
0.1 < delta < 0.5 | up |
delta >= 0.5 | introduced |
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:
VexConsensus(3.0.9-1)= 0.45 (CVE-2026-12345 present)VexConsensus(3.0.9-1+deb12u3)= 0.95 (CVE patched)ReachablePaths(before)= 3ReachablePaths(after)= 0PatchVerificationConfidence= 0.97SymbolSimilarity= 0.85HasDsseAttestation= trueIssuerAuthorityScore= 0.90
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:
VexConsensus(3.0.8)= 0.85VexConsensus(3.1.0)= 0.55 (regression CVE)ReachablePaths(before)= 0ReachablePaths(after)= 5- No patch verification (upgrade, not patch)
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:
VexConsensus(1.0.0)= 0.90 (both versions)ReachablePaths= 2 (both versions)SymbolSimilarity= 1.0 (identical code)
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
- CVE Context: List CVEs affecting the package
- Version Change: Document version transition
- Patch Verification: Report verification method and confidence
- Symbol Similarity: Report code similarity metrics
- Reachability: Report call path changes
- Attestation: Note DSSE attestation presence
- 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:
- Same VEX consensus data
- Same reachability data
- Same patch verification results
- Same options
Floating Point Handling
- All intermediate calculations use
doubleprecision - Final scores are rounded to 2 decimal places
- Comparisons use exact equality after rounding
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
- v1.0: initial release.
- Future versions follow semantic versioning.
- Breaking changes to the formula increment the major version of this contract.
References
- Change-Trace Schema Contract
- Change-Trace Architecture
- VexLens Architecture — trust scoring / consensus
- ReachGraph Architecture — reachability analysis
