Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,23 @@
# AGENTS
## Role
Centralizes policy configuration, provider trust weights, and justification guardrails applied to Excititor consensus decisions.
## Scope
- Policy models for tier weighting, provider overrides, justification allowlists, and conflict escalation.
- Configuration binding helpers (YAML/JSON) and validation of operator-supplied policy bundles.
- Evaluation services that expose policy revisions and change tracking to WebService/Worker.
- Documentation anchors for policy schema and upgrade guidance.
## Participants
- WebService consumes policy bindings to authorize ingest/export operations and to recompute consensus.
- Worker schedules reconciliation runs using policy revisions from this module.
- CLI exposes policy inspection commands based on exported descriptors.
## Interfaces & contracts
- `IVexPolicyProvider`, `IVexPolicyEvaluator`, and immutable policy snapshot value objects.
- Validation diagnostics APIs surfacing structured errors and warnings for operators.
## In/Out of scope
In: policy schema definition, binding/validation, evaluation utilities, audit logging helpers.
Out: persistence/migrations, HTTP exposure, connector-specific trust logic (lives in Core/Connectors).
## Observability & security expectations
- Emit structured events on policy load/update with revision IDs, but do not log full sensitive policy documents.
- Maintain deterministic error ordering for reproducible diagnostics.
## Tests
- Policy fixtures and regression coverage will live in `../StellaOps.Excititor.Policy.Tests` once scaffolded; leverage snapshot comparisons for YAML bindings.

View File

@@ -0,0 +1,161 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Core;
namespace StellaOps.Excititor.Policy;
public interface IVexPolicyProvider
{
VexPolicySnapshot GetSnapshot();
}
public interface IVexPolicyEvaluator
{
string Version { get; }
VexPolicySnapshot Snapshot { get; }
double GetProviderWeight(VexProvider provider);
bool IsClaimEligible(VexClaim claim, VexProvider provider, out string? rejectionReason);
}
public sealed record VexPolicySnapshot(
string Version,
VexConsensusPolicyOptions ConsensusOptions,
IVexConsensusPolicy ConsensusPolicy,
ImmutableArray<VexPolicyIssue> Issues,
string RevisionId,
string Digest)
{
public static readonly VexPolicySnapshot Default = new(
VexConsensusPolicyOptions.BaselineVersion,
new VexConsensusPolicyOptions(),
new BaselineVexConsensusPolicy(),
ImmutableArray<VexPolicyIssue>.Empty,
"rev-0",
string.Empty);
}
public sealed record VexPolicyIssue(
string Code,
string Message,
VexPolicyIssueSeverity Severity);
public enum VexPolicyIssueSeverity
{
Warning,
Error,
}
public sealed class VexPolicyProvider : IVexPolicyProvider
{
private readonly IOptionsMonitor<VexPolicyOptions> _options;
private readonly ILogger<VexPolicyProvider> _logger;
private readonly object _sync = new();
private long _revisionCounter;
private string? _currentRevisionId;
private string? _currentDigest;
public VexPolicyProvider(
IOptionsMonitor<VexPolicyOptions> options,
ILogger<VexPolicyProvider> logger)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public VexPolicySnapshot GetSnapshot()
{
var options = _options.CurrentValue ?? new VexPolicyOptions();
return BuildSnapshot(options);
}
private VexPolicySnapshot BuildSnapshot(VexPolicyOptions options)
{
var normalization = VexPolicyProcessing.Normalize(options);
var digest = VexPolicyDigest.Compute(normalization.ConsensusOptions);
string revisionId;
bool isNewRevision;
lock (_sync)
{
if (!string.Equals(_currentDigest, digest, StringComparison.Ordinal))
{
_revisionCounter++;
revisionId = $"rev-{_revisionCounter}";
_currentDigest = digest;
_currentRevisionId = revisionId;
isNewRevision = true;
}
else
{
revisionId = _currentRevisionId ?? "rev-0";
isNewRevision = false;
}
}
var policy = new BaselineVexConsensusPolicy(normalization.ConsensusOptions);
var snapshot = new VexPolicySnapshot(
normalization.ConsensusOptions.Version,
normalization.ConsensusOptions,
policy,
normalization.Issues,
revisionId,
digest);
if (isNewRevision)
{
_logger.LogInformation(
"Policy snapshot updated: revision {RevisionId}, version {Version}, digest {Digest}, issues {IssueCount}",
snapshot.RevisionId,
snapshot.Version,
snapshot.Digest,
snapshot.Issues.Length);
VexPolicyTelemetry.RecordReload(snapshot.RevisionId, snapshot.Version, snapshot.Issues.Length);
}
else if (snapshot.Issues.Length > 0)
{
foreach (var issue in snapshot.Issues)
{
_logger.LogWarning("Policy issue {Code}: {Message}", issue.Code, issue.Message);
}
}
return snapshot;
}
}
public sealed class VexPolicyEvaluator : IVexPolicyEvaluator
{
private readonly IVexPolicyProvider _provider;
public VexPolicyEvaluator(IVexPolicyProvider provider)
{
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
}
public string Version => Snapshot.Version;
public VexPolicySnapshot Snapshot => _provider.GetSnapshot();
public double GetProviderWeight(VexProvider provider)
=> Snapshot.ConsensusPolicy.GetProviderWeight(provider);
public bool IsClaimEligible(VexClaim claim, VexProvider provider, out string? rejectionReason)
=> Snapshot.ConsensusPolicy.IsClaimEligible(claim, provider, out rejectionReason);
}
public static class VexPolicyServiceCollectionExtensions
{
public static IServiceCollection AddVexPolicy(this IServiceCollection services)
{
services.AddSingleton<IVexPolicyProvider, VexPolicyProvider>();
services.AddSingleton<IVexPolicyEvaluator, VexPolicyEvaluator>();
return services;
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="YamlDotNet" Version="13.7.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-POLICY-01-001 Policy schema & binding|Team Excititor Policy|EXCITITOR-CORE-01-001|DONE (2025-10-15) Established `VexPolicyOptions`, options binding, and snapshot provider covering baseline weights/overrides.|
|EXCITITOR-POLICY-01-002 Policy evaluator service|Team Excititor Policy|EXCITITOR-POLICY-01-001|DONE (2025-10-15) `VexPolicyEvaluator` exposes immutable snapshots to consensus and normalizes rejection reasons.|
|EXCITITOR-POLICY-01-003 Operator diagnostics & docs|Team Excititor Policy|EXCITITOR-POLICY-01-001|**DONE (2025-10-16)** Surface structured diagnostics (CLI/WebService) and author policy upgrade guidance in docs/ARCHITECTURE_EXCITITOR.md appendix.<br>2025-10-16: Added `IVexPolicyDiagnostics`/`VexPolicyDiagnosticsReport`, sorted issue ordering, recommendations, and appendix guidance. Tests: `dotnet test src/Excititor/__Tests/StellaOps.Excititor.Core.Tests/StellaOps.Excititor.Core.Tests.csproj`.|
|EXCITITOR-POLICY-01-004 Policy schema validation & YAML binding|Team Excititor Policy|EXCITITOR-POLICY-01-001|**DONE (2025-10-16)** Added strongly-typed YAML/JSON binding, schema validation, and deterministic diagnostics for operator-supplied policy bundles.|
|EXCITITOR-POLICY-01-005 Policy change tracking & telemetry|Team Excititor Policy|EXCITITOR-POLICY-01-002|**DONE (2025-10-16)** Emit revision history, expose snapshot digests via CLI/WebService, and add structured logging/metrics for policy reloads.<br>2025-10-16: `VexPolicySnapshot` now carries revision/digest, provider logs reloads, `vex.policy.reloads` metric emitted, binder/diagnostics expose digest metadata. Tests: `dotnet test src/Excititor/__Tests/StellaOps.Excititor.Core.Tests/StellaOps.Excititor.Core.Tests.csproj`.|
|EXCITITOR-POLICY-02-001 Scoring coefficients & weight ceilings|Team Excititor Policy|EXCITITOR-POLICY-01-004|DONE (2025-10-19) Added `weights.ceiling` + `scoring.{alpha,beta}` options with normalization warnings, extended consensus policy/digest, refreshed docs (`docs/ARCHITECTURE_EXCITITOR.md`, `docs/EXCITITOR_SCORRING.md`), and validated via `dotnet test` for core/policy suites.|
|EXCITITOR-POLICY-02-002 Diagnostics for scoring signals|Team Excititor Policy|EXCITITOR-POLICY-02-001|BACKLOG Update diagnostics reports to surface missing severity/KEV/EPSS mappings, coefficient overrides, and provide actionable recommendations for policy tuning.|

View File

@@ -0,0 +1,94 @@
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using StellaOps.Excititor.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace StellaOps.Excititor.Policy;
public enum VexPolicyDocumentFormat
{
Json,
Yaml,
}
public sealed record VexPolicyBindingResult(
bool Success,
VexPolicyOptions? Options,
VexConsensusPolicyOptions? NormalizedOptions,
ImmutableArray<VexPolicyIssue> Issues);
public static class VexPolicyBinder
{
public static VexPolicyBindingResult Bind(string content, VexPolicyDocumentFormat format)
{
if (string.IsNullOrWhiteSpace(content))
{
return Failure("policy.empty", "Policy document is empty.");
}
try
{
var options = Parse(content, format);
return Normalize(options);
}
catch (JsonException ex)
{
return Failure("policy.parse.json", $"Failed to parse JSON policy document: {ex.Message}");
}
catch (YamlDotNet.Core.YamlException ex)
{
return Failure("policy.parse.yaml", $"Failed to parse YAML policy document: {ex.Message}");
}
}
public static VexPolicyBindingResult Bind(Stream stream, VexPolicyDocumentFormat format, Encoding? encoding = null)
{
if (stream is null)
{
throw new ArgumentNullException(nameof(stream));
}
encoding ??= Encoding.UTF8;
using var reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
var content = reader.ReadToEnd();
return Bind(content, format);
}
private static VexPolicyBindingResult Normalize(VexPolicyOptions options)
{
var normalization = VexPolicyProcessing.Normalize(options);
var hasErrors = normalization.Issues.Any(static issue => issue.Severity == VexPolicyIssueSeverity.Error);
return new VexPolicyBindingResult(!hasErrors, options, normalization.ConsensusOptions, normalization.Issues);
}
private static VexPolicyBindingResult Failure(string code, string message)
{
var issue = new VexPolicyIssue(code, message, VexPolicyIssueSeverity.Error);
return new VexPolicyBindingResult(false, null, null, ImmutableArray.Create(issue));
}
private static VexPolicyOptions Parse(string content, VexPolicyDocumentFormat format)
{
return format switch
{
VexPolicyDocumentFormat.Json => JsonSerializer.Deserialize<VexPolicyOptions>(content, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
}) ?? new VexPolicyOptions(),
VexPolicyDocumentFormat.Yaml => BuildYamlDeserializer().Deserialize<VexPolicyOptions>(content) ?? new VexPolicyOptions(),
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported policy document format."),
};
}
private static IDeserializer BuildYamlDeserializer()
=> new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Excititor.Policy;
public interface IVexPolicyDiagnostics
{
VexPolicyDiagnosticsReport GetDiagnostics();
}
public sealed record VexPolicyDiagnosticsReport(
string Version,
string RevisionId,
string Digest,
int ErrorCount,
int WarningCount,
DateTimeOffset GeneratedAt,
ImmutableArray<VexPolicyIssue> Issues,
ImmutableArray<string> Recommendations,
ImmutableDictionary<string, double> ActiveOverrides);
public sealed class VexPolicyDiagnostics : IVexPolicyDiagnostics
{
private readonly IVexPolicyProvider _policyProvider;
private readonly TimeProvider _timeProvider;
public VexPolicyDiagnostics(
IVexPolicyProvider policyProvider,
TimeProvider? timeProvider = null)
{
_policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public VexPolicyDiagnosticsReport GetDiagnostics()
{
var snapshot = _policyProvider.GetSnapshot();
var issues = snapshot.Issues;
var errorCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Error);
var warningCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Warning);
var overrides = snapshot.ConsensusOptions.ProviderOverrides
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.ToImmutableDictionary();
var recommendations = BuildRecommendations(errorCount, warningCount, overrides);
return new VexPolicyDiagnosticsReport(
snapshot.Version,
snapshot.RevisionId,
snapshot.Digest,
errorCount,
warningCount,
_timeProvider.GetUtcNow(),
issues,
recommendations,
overrides);
}
private static ImmutableArray<string> BuildRecommendations(
int errorCount,
int warningCount,
ImmutableDictionary<string, double> overrides)
{
var messages = ImmutableArray.CreateBuilder<string>();
if (errorCount > 0)
{
messages.Add("Resolve policy errors before running consensus; defaults are used while errors persist.");
}
if (warningCount > 0)
{
messages.Add("Review policy warnings via CLI/Web diagnostics and adjust configuration as needed.");
}
if (overrides.Count > 0)
{
messages.Add($"Provider overrides active for: {string.Join(", ", overrides.Keys)}.");
}
messages.Add("Refer to docs/ARCHITECTURE_EXCITITOR.md for policy upgrade and diagnostics guidance.");
return messages.ToImmutable();
}
}

View File

@@ -0,0 +1,38 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using StellaOps.Excititor.Core;
namespace StellaOps.Excititor.Policy;
internal static class VexPolicyDigest
{
public static string Compute(VexConsensusPolicyOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var builder = new StringBuilder();
builder.Append(options.Version).Append('|')
.Append(options.VendorWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
.Append(options.DistroWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
.Append(options.PlatformWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
.Append(options.HubWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
.Append(options.AttestationWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
.Append(options.WeightCeiling.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
.Append(options.Alpha.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
.Append(options.Beta.ToString("F6", CultureInfo.InvariantCulture));
foreach (var kvp in options.ProviderOverrides
.OrderBy(static pair => pair.Key, StringComparer.Ordinal))
{
builder.Append('|')
.Append(kvp.Key)
.Append('=')
.Append(kvp.Value.ToString("F6", CultureInfo.InvariantCulture));
}
var input = builder.ToString();
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(hash);
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
namespace StellaOps.Excititor.Policy;
public sealed class VexPolicyOptions
{
public string? Version { get; set; }
public VexPolicyWeightOptions Weights { get; set; } = new();
public VexPolicyScoringOptions Scoring { get; set; } = new();
public IDictionary<string, double>? ProviderOverrides { get; set; }
}
public sealed class VexPolicyWeightOptions
{
public double? Vendor { get; set; }
public double? Distro { get; set; }
public double? Platform { get; set; }
public double? Hub { get; set; }
public double? Attestation { get; set; }
public double? Ceiling { get; set; }
}
public sealed class VexPolicyScoringOptions
{
public double? Alpha { get; set; }
public double? Beta { get; set; }
}

View File

@@ -0,0 +1,282 @@
using System.Collections.Immutable;
using System.Globalization;
using StellaOps.Excititor.Core;
namespace StellaOps.Excititor.Policy;
internal static class VexPolicyProcessing
{
private const double DefaultVendorWeight = 1.0;
private const double DefaultDistroWeight = 0.9;
private const double DefaultPlatformWeight = 0.7;
private const double DefaultHubWeight = 0.5;
private const double DefaultAttestationWeight = 0.6;
public static VexPolicyNormalizationResult Normalize(VexPolicyOptions? options)
{
var issues = ImmutableArray.CreateBuilder<VexPolicyIssue>();
var policyOptions = options ?? new VexPolicyOptions();
var normalizedWeights = NormalizeWeights(policyOptions.Weights, issues);
var overrides = NormalizeOverrides(policyOptions.ProviderOverrides, normalizedWeights.Ceiling, issues);
var scoring = NormalizeScoring(policyOptions.Scoring, issues);
var consensusOptions = new VexConsensusPolicyOptions(
policyOptions.Version ?? VexConsensusPolicyOptions.BaselineVersion,
normalizedWeights.Vendor,
normalizedWeights.Distro,
normalizedWeights.Platform,
normalizedWeights.Hub,
normalizedWeights.Attestation,
overrides,
normalizedWeights.Ceiling,
scoring.Alpha,
scoring.Beta);
var orderedIssues = issues.ToImmutable().Sort(IssueComparer);
return new VexPolicyNormalizationResult(consensusOptions, orderedIssues);
}
public static ImmutableArray<VexPolicyIssue> SortIssues(IEnumerable<VexPolicyIssue> issues)
=> issues.ToImmutableArray().Sort(IssueComparer);
private static WeightNormalizationResult NormalizeWeights(
VexPolicyWeightOptions? options,
ImmutableArray<VexPolicyIssue>.Builder issues)
{
var ceiling = NormalizeWeightCeiling(options?.Ceiling, issues);
var vendor = NormalizeWeightValue(
options?.Vendor,
"vendor",
DefaultVendorWeight,
ceiling,
issues);
var distro = NormalizeWeightValue(
options?.Distro,
"distro",
DefaultDistroWeight,
ceiling,
issues);
var platform = NormalizeWeightValue(
options?.Platform,
"platform",
DefaultPlatformWeight,
ceiling,
issues);
var hub = NormalizeWeightValue(
options?.Hub,
"hub",
DefaultHubWeight,
ceiling,
issues);
var attestation = NormalizeWeightValue(
options?.Attestation,
"attestation",
DefaultAttestationWeight,
ceiling,
issues);
return new WeightNormalizationResult(vendor, distro, platform, hub, attestation, ceiling);
}
private static double NormalizeWeightValue(
double? value,
string fieldName,
double defaultValue,
double ceiling,
ImmutableArray<VexPolicyIssue>.Builder issues)
{
if (value is null)
{
return defaultValue;
}
if (double.IsNaN(value.Value) || double.IsInfinity(value.Value))
{
issues.Add(new VexPolicyIssue(
$"weights.{fieldName}.invalid",
$"{fieldName} must be a finite number; default {defaultValue.ToString(CultureInfo.InvariantCulture)} applied.",
VexPolicyIssueSeverity.Warning));
return defaultValue;
}
if (value.Value < 0 || value.Value > ceiling)
{
issues.Add(new VexPolicyIssue(
$"weights.{fieldName}.range",
$"{fieldName} must be between 0 and {ceiling.ToString(CultureInfo.InvariantCulture)}; value {value.Value.ToString(CultureInfo.InvariantCulture)} was clamped.",
VexPolicyIssueSeverity.Warning));
return Math.Clamp(value.Value, 0, ceiling);
}
return value.Value;
}
private static double NormalizeWeightCeiling(double? ceiling, ImmutableArray<VexPolicyIssue>.Builder issues)
{
if (ceiling is null)
{
return VexConsensusPolicyOptions.DefaultWeightCeiling;
}
if (double.IsNaN(ceiling.Value) || double.IsInfinity(ceiling.Value) || ceiling.Value <= 0)
{
issues.Add(new VexPolicyIssue(
"weights.ceiling.invalid",
"weights.ceiling must be a positive, finite number; default ceiling applied.",
VexPolicyIssueSeverity.Warning));
return VexConsensusPolicyOptions.DefaultWeightCeiling;
}
if (ceiling.Value < 1)
{
issues.Add(new VexPolicyIssue(
"weights.ceiling.minimum",
"weights.ceiling below 1 falls back to 1 to preserve baseline behaviour.",
VexPolicyIssueSeverity.Warning));
return 1;
}
if (ceiling.Value > VexConsensusPolicyOptions.MaxSupportedCeiling)
{
issues.Add(new VexPolicyIssue(
"weights.ceiling.maximum",
$"weights.ceiling exceeded supported range; value {ceiling.Value.ToString(CultureInfo.InvariantCulture)} was clamped to {VexConsensusPolicyOptions.MaxSupportedCeiling.ToString(CultureInfo.InvariantCulture)}.",
VexPolicyIssueSeverity.Warning));
return VexConsensusPolicyOptions.MaxSupportedCeiling;
}
return ceiling.Value;
}
private static ImmutableDictionary<string, double> NormalizeOverrides(
IDictionary<string, double>? overrides,
double ceiling,
ImmutableArray<VexPolicyIssue>.Builder issues)
{
if (overrides is null || overrides.Count == 0)
{
return ImmutableDictionary<string, double>.Empty;
}
var builder = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.Ordinal);
foreach (var kvp in overrides)
{
if (string.IsNullOrWhiteSpace(kvp.Key))
{
issues.Add(new VexPolicyIssue(
"overrides.key.missing",
"Encountered provider override with empty key; ignoring entry.",
VexPolicyIssueSeverity.Warning));
continue;
}
var key = kvp.Key.Trim();
var weight = NormalizeWeightValue(
kvp.Value,
$"overrides.{key}",
DefaultVendorWeight,
ceiling,
issues);
builder[key] = weight;
}
return builder.ToImmutable();
}
private static ScoringNormalizationResult NormalizeScoring(
VexPolicyScoringOptions? options,
ImmutableArray<VexPolicyIssue>.Builder issues)
{
var alpha = NormalizeCoefficient(
options?.Alpha,
"alpha",
VexConsensusPolicyOptions.DefaultAlpha,
issues);
var beta = NormalizeCoefficient(
options?.Beta,
"beta",
VexConsensusPolicyOptions.DefaultBeta,
issues);
return new ScoringNormalizationResult(alpha, beta);
}
private static double NormalizeCoefficient(
double? value,
string fieldName,
double defaultValue,
ImmutableArray<VexPolicyIssue>.Builder issues)
{
if (value is null)
{
return defaultValue;
}
if (double.IsNaN(value.Value) || double.IsInfinity(value.Value))
{
issues.Add(new VexPolicyIssue(
$"scoring.{fieldName}.invalid",
$"{fieldName} coefficient must be a finite number; default {defaultValue.ToString(CultureInfo.InvariantCulture)} applied.",
VexPolicyIssueSeverity.Warning));
return defaultValue;
}
if (value.Value < 0)
{
issues.Add(new VexPolicyIssue(
$"scoring.{fieldName}.range",
$"{fieldName} cannot be negative; default {defaultValue.ToString(CultureInfo.InvariantCulture)} applied.",
VexPolicyIssueSeverity.Warning));
return defaultValue;
}
if (value.Value > VexConsensusPolicyOptions.MaxSupportedCoefficient)
{
issues.Add(new VexPolicyIssue(
$"scoring.{fieldName}.maximum",
$"{fieldName} exceeded supported range; value {value.Value.ToString(CultureInfo.InvariantCulture)} was clamped to {VexConsensusPolicyOptions.MaxSupportedCoefficient.ToString(CultureInfo.InvariantCulture)}.",
VexPolicyIssueSeverity.Warning));
return VexConsensusPolicyOptions.MaxSupportedCoefficient;
}
return value.Value;
}
private static int CompareIssues(VexPolicyIssue left, VexPolicyIssue right)
{
var severityCompare = GetSeverityRank(left.Severity).CompareTo(GetSeverityRank(right.Severity));
if (severityCompare != 0)
{
return severityCompare;
}
return string.Compare(left.Code, right.Code, StringComparison.Ordinal);
}
private static int GetSeverityRank(VexPolicyIssueSeverity severity)
=> severity switch
{
VexPolicyIssueSeverity.Error => 0,
VexPolicyIssueSeverity.Warning => 1,
_ => 2,
};
private static readonly Comparer<VexPolicyIssue> IssueComparer = Comparer<VexPolicyIssue>.Create(CompareIssues);
internal sealed record VexPolicyNormalizationResult(
VexConsensusPolicyOptions ConsensusOptions,
ImmutableArray<VexPolicyIssue> Issues);
private sealed record WeightNormalizationResult(
double Vendor,
double Distro,
double Platform,
double Hub,
double Attestation,
double Ceiling);
private sealed record ScoringNormalizationResult(double Alpha, double Beta);
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Diagnostics.Metrics;
namespace StellaOps.Excititor.Policy;
internal static class VexPolicyTelemetry
{
private const string MeterName = "StellaOps.Excititor.Policy";
private const string MeterVersion = "1.0.0";
private static readonly Meter Meter = new(MeterName, MeterVersion);
private static readonly Counter<long> PolicyReloads = Meter.CreateCounter<long>("vex.policy.reloads", unit: "events");
public static void RecordReload(string revisionId, string version, int issueCount)
{
var tags = new KeyValuePair<string, object?>[]
{
new("revision", revisionId),
new("version", version),
new("issues", issueCount),
};
PolicyReloads.Add(1, tags);
}
}