namespace StellaOps.VexLens.Trust.SourceTrust; /// /// Default implementation of . /// Applies time-based trust decay, recency bonuses, and revocation penalties. /// public sealed class TrustDecayService : ITrustDecayService { public DecayResult ApplyDecay( double baseScore, DateTimeOffset statementTimestamp, DecayContext context) { var age = context.EvaluationTime - statementTimestamp; var ageDays = age.TotalDays; var config = context.Configuration; var (decayFactor, category) = CalculateDecayFactor(age, config); // Reduce decay for statements with updates if (context.HasUpdates && context.UpdateCount > 0) { // Each update reduces effective age by 10%, up to 50% reduction var updateReduction = Math.Min(0.5, context.UpdateCount * 0.1); decayFactor = Math.Min(1.0, decayFactor + (1.0 - decayFactor) * updateReduction); } var decayedScore = baseScore * decayFactor; return new DecayResult { BaseScore = baseScore, DecayFactor = decayFactor, DecayedScore = decayedScore, AgeDays = ageDays, Category = category }; } public double CalculateRecencyBonus( DateTimeOffset lastUpdateTimestamp, RecencyBonusContext context) { var age = context.EvaluationTime - lastUpdateTimestamp; if (age > context.RecencyWindow) { return 0.0; } // Linear decrease from max bonus to 0 over the recency window var ratio = 1.0 - (age.TotalSeconds / context.RecencyWindow.TotalSeconds); return context.MaxBonus * ratio; } public RevocationImpact CalculateRevocationImpact( RevocationInfo revocation, RevocationContext context) { if (!revocation.IsRevoked) { return new RevocationImpact { ShouldExclude = false, Penalty = 0.0, Explanation = "Statement is not revoked", RecommendedAction = RevocationAction.None }; } // Determine impact based on revocation type return revocation.RevocationType switch { RevocationType.Superseded when revocation.WasSuperseded => new RevocationImpact { ShouldExclude = true, Penalty = context.SupersededPenalty, Explanation = $"Statement superseded by {revocation.SupersededBy ?? "newer statement"}", RecommendedAction = RevocationAction.Replace }, RevocationType.Correction => new RevocationImpact { ShouldExclude = false, Penalty = context.CorrectionPenalty, Explanation = $"Statement corrected: {revocation.RevocationReason ?? "unspecified reason"}", RecommendedAction = RevocationAction.Penalize }, RevocationType.Withdrawn => new RevocationImpact { ShouldExclude = true, Penalty = context.RevocationPenalty, Explanation = $"Statement withdrawn: {revocation.RevocationReason ?? "unspecified reason"}", RecommendedAction = RevocationAction.Exclude }, RevocationType.Expired => new RevocationImpact { ShouldExclude = false, Penalty = context.RevocationPenalty * 0.5, Explanation = "Statement expired and was not renewed", RecommendedAction = RevocationAction.Review }, RevocationType.SourceRevoked => new RevocationImpact { ShouldExclude = true, Penalty = context.RevocationPenalty, Explanation = "Source has been revoked", RecommendedAction = RevocationAction.Exclude }, _ => new RevocationImpact { ShouldExclude = false, Penalty = context.RevocationPenalty * 0.75, Explanation = $"Statement revoked: {revocation.RevocationReason ?? "unknown reason"}", RecommendedAction = RevocationAction.Review } }; } public EffectiveTrustScore GetEffectiveScore( double baseScore, TrustScoreFactors factors, DateTimeOffset evaluationTime) { var adjustments = new List(); var shouldExclude = false; // Apply decay var decayConfig = factors.DecayConfiguration ?? DecayConfiguration.CreateDefault(); var decayContext = new DecayContext { EvaluationTime = evaluationTime, Configuration = decayConfig, HasUpdates = factors.UpdateCount > 0, UpdateCount = factors.UpdateCount }; var decayResult = ApplyDecay(baseScore, factors.StatementTimestamp, decayContext); adjustments.Add(new TrustAdjustment { Type = TrustAdjustmentType.Decay, Amount = decayResult.DecayedScore - baseScore, Reason = $"Time-based decay (age: {decayResult.AgeDays:F1} days, category: {decayResult.Category})" }); var effectiveScore = decayResult.DecayedScore; // Apply recency bonus if recently updated var recencyBonus = 0.0; if (factors.LastUpdateTimestamp.HasValue) { var recencyContext = new RecencyBonusContext { EvaluationTime = evaluationTime }; recencyBonus = CalculateRecencyBonus(factors.LastUpdateTimestamp.Value, recencyContext); if (recencyBonus > 0) { effectiveScore += recencyBonus; adjustments.Add(new TrustAdjustment { Type = TrustAdjustmentType.RecencyBonus, Amount = recencyBonus, Reason = "Recently updated statement" }); } } // Apply update bonus if (factors.UpdateCount > 1) { var updateBonus = Math.Min(0.05, factors.UpdateCount * 0.01); effectiveScore += updateBonus; adjustments.Add(new TrustAdjustment { Type = TrustAdjustmentType.UpdateBonus, Amount = updateBonus, Reason = $"Statement has been updated {factors.UpdateCount} times" }); } // Apply revocation penalty var revocationPenalty = 0.0; if (factors.Revocation != null) { var revocationContext = new RevocationContext { EvaluationTime = evaluationTime }; var revocationImpact = CalculateRevocationImpact(factors.Revocation, revocationContext); if (revocationImpact.ShouldExclude) { shouldExclude = true; } revocationPenalty = revocationImpact.Penalty; effectiveScore -= revocationPenalty; adjustments.Add(new TrustAdjustment { Type = TrustAdjustmentType.RevocationPenalty, Amount = -revocationPenalty, Reason = revocationImpact.Explanation }); } // Clamp final score effectiveScore = Math.Clamp(effectiveScore, 0.0, 1.0); return new EffectiveTrustScore { BaseScore = baseScore, EffectiveScore = effectiveScore, DecayFactor = decayResult.DecayFactor, RecencyBonus = recencyBonus, RevocationPenalty = revocationPenalty, ShouldExclude = shouldExclude, StalenessCategory = decayResult.Category, Adjustments = adjustments }; } private (double Factor, StalenessCategory Category) CalculateDecayFactor( TimeSpan age, DecayConfiguration config) { if (age <= TimeSpan.Zero) { return (1.0, StalenessCategory.Fresh); } if (age < config.FreshThreshold) { return (1.0, StalenessCategory.Fresh); } if (age < config.RecentThreshold) { var factor = CalculateCurveValue( age, config.FreshThreshold, config.RecentThreshold, 1.0, 0.9, config.CurveType); return (factor, StalenessCategory.Recent); } if (age < config.StaleThreshold) { var factor = CalculateCurveValue( age, config.RecentThreshold, config.StaleThreshold, 0.9, 0.7, config.CurveType); return (factor, StalenessCategory.Aging); } if (age < config.ExpiredThreshold) { var factor = CalculateCurveValue( age, config.StaleThreshold, config.ExpiredThreshold, 0.7, config.MinDecayFactor, config.CurveType); return (factor, StalenessCategory.Stale); } return (config.MinDecayFactor, StalenessCategory.Expired); } private static double CalculateCurveValue( TimeSpan current, TimeSpan start, TimeSpan end, double startValue, double endValue, DecayCurveType curveType) { var progress = (current - start).TotalSeconds / (end - start).TotalSeconds; progress = Math.Clamp(progress, 0.0, 1.0); return curveType switch { DecayCurveType.Linear => startValue + (endValue - startValue) * progress, DecayCurveType.Exponential => startValue * Math.Pow(endValue / startValue, progress), DecayCurveType.Step => progress < 0.5 ? startValue : endValue, _ => startValue + (endValue - startValue) * progress }; } }