up
Some checks failed
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 09:37:15 +02:00
parent e00f6365da
commit 6e45066e37
349 changed files with 17160 additions and 1867 deletions

View File

@@ -0,0 +1,202 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Messaging;
using StellaOps.Messaging.Abstractions;
using StellaOps.Policy.Engine.Options;
namespace StellaOps.Policy.Engine.Caching;
/// <summary>
/// Policy evaluation cache backed by <see cref="IDistributedCache{TValue}"/>.
/// Supports any transport (InMemory, Valkey, PostgreSQL) via factory injection.
/// </summary>
public sealed class MessagingPolicyEvaluationCache : IPolicyEvaluationCache
{
private readonly IDistributedCache<PolicyEvaluationCacheEntry> _cache;
private readonly TimeProvider _timeProvider;
private readonly ILogger<MessagingPolicyEvaluationCache> _logger;
private readonly TimeSpan _defaultTtl;
private long _totalRequests;
private long _cacheHits;
private long _cacheMisses;
public MessagingPolicyEvaluationCache(
IDistributedCacheFactory cacheFactory,
ILogger<MessagingPolicyEvaluationCache> logger,
TimeProvider timeProvider,
IOptions<PolicyEngineOptions> options)
{
ArgumentNullException.ThrowIfNull(cacheFactory);
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
var cacheOptions = options?.Value.EvaluationCache ?? new PolicyEvaluationCacheOptions();
_defaultTtl = TimeSpan.FromMinutes(cacheOptions.DefaultTtlMinutes);
_cache = cacheFactory.Create<PolicyEvaluationCacheEntry>(new CacheOptions
{
KeyPrefix = "pe:",
DefaultTtl = _defaultTtl,
});
_logger.LogInformation(
"Initialized MessagingPolicyEvaluationCache with provider {Provider}, TTL {Ttl}",
_cache.ProviderName,
_defaultTtl);
}
public async Task<PolicyEvaluationCacheResult> GetAsync(
PolicyEvaluationCacheKey key,
CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref _totalRequests);
var cacheKey = key.ToCacheKey();
var result = await _cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false);
if (result.HasValue)
{
var entry = result.Value;
var now = _timeProvider.GetUtcNow();
// Check if entry is still valid
if (entry.ExpiresAt > now)
{
Interlocked.Increment(ref _cacheHits);
return new PolicyEvaluationCacheResult(entry, true, MapSource(_cache.ProviderName));
}
// Entry expired - remove it
await _cache.InvalidateAsync(cacheKey, cancellationToken).ConfigureAwait(false);
}
Interlocked.Increment(ref _cacheMisses);
return new PolicyEvaluationCacheResult(null, false, CacheSource.None);
}
public async Task<PolicyEvaluationCacheBatch> GetBatchAsync(
IReadOnlyList<PolicyEvaluationCacheKey> keys,
CancellationToken cancellationToken = default)
{
var found = new Dictionary<PolicyEvaluationCacheKey, PolicyEvaluationCacheEntry>();
var notFound = new List<PolicyEvaluationCacheKey>();
var hits = 0;
var misses = 0;
foreach (var key in keys)
{
var result = await GetAsync(key, cancellationToken).ConfigureAwait(false);
if (result.Entry != null)
{
found[key] = result.Entry;
hits++;
}
else
{
notFound.Add(key);
misses++;
}
}
var source = MapSource(_cache.ProviderName);
return new PolicyEvaluationCacheBatch
{
Found = found,
NotFound = notFound,
CacheHits = hits,
CacheMisses = misses,
InMemoryHits = source == CacheSource.InMemory ? hits : 0,
RedisHits = source == CacheSource.Redis ? hits : 0,
};
}
public async Task SetAsync(
PolicyEvaluationCacheKey key,
PolicyEvaluationCacheEntry entry,
CancellationToken cancellationToken = default)
{
var cacheKey = key.ToCacheKey();
var now = _timeProvider.GetUtcNow();
var expiresAt = entry.ExpiresAt > now ? entry.ExpiresAt : now.Add(_defaultTtl);
var ttl = expiresAt - now;
if (ttl <= TimeSpan.Zero)
{
return;
}
var options = new CacheEntryOptions { TimeToLive = ttl };
await _cache.SetAsync(cacheKey, entry, options, cancellationToken).ConfigureAwait(false);
}
public async Task SetBatchAsync(
IReadOnlyDictionary<PolicyEvaluationCacheKey, PolicyEvaluationCacheEntry> entries,
CancellationToken cancellationToken = default)
{
var now = _timeProvider.GetUtcNow();
foreach (var (key, entry) in entries)
{
var cacheKey = key.ToCacheKey();
var expiresAt = entry.ExpiresAt > now ? entry.ExpiresAt : now.Add(_defaultTtl);
var ttl = expiresAt - now;
if (ttl <= TimeSpan.Zero)
{
continue;
}
var options = new CacheEntryOptions { TimeToLive = ttl };
await _cache.SetAsync(cacheKey, entry, options, cancellationToken).ConfigureAwait(false);
}
}
public async Task InvalidateAsync(
PolicyEvaluationCacheKey key,
CancellationToken cancellationToken = default)
{
var cacheKey = key.ToCacheKey();
await _cache.InvalidateAsync(cacheKey, cancellationToken).ConfigureAwait(false);
}
public async Task InvalidateByPolicyDigestAsync(
string policyDigest,
CancellationToken cancellationToken = default)
{
// Pattern: pe:<policyDigest>:*
var pattern = $"{policyDigest}:*";
var count = await _cache.InvalidateByPatternAsync(pattern, cancellationToken).ConfigureAwait(false);
_logger.LogDebug(
"Invalidated {Count} cache entries for policy digest {Digest}",
count,
policyDigest);
}
public PolicyEvaluationCacheStats GetStats()
{
var source = MapSource(_cache.ProviderName);
var hits = Interlocked.Read(ref _cacheHits);
return new PolicyEvaluationCacheStats
{
TotalRequests = Interlocked.Read(ref _totalRequests),
CacheHits = hits,
CacheMisses = Interlocked.Read(ref _cacheMisses),
InMemoryHits = source == CacheSource.InMemory ? hits : 0,
RedisHits = source == CacheSource.Redis ? hits : 0,
RedisFallbacks = 0,
ItemCount = 0, // Not available from IDistributedCache
EvictionCount = 0, // Not available from IDistributedCache
};
}
private static CacheSource MapSource(string providerName) => providerName.ToLowerInvariant() switch
{
"inmemory" => CacheSource.InMemory,
"valkey" => CacheSource.Redis,
"redis" => CacheSource.Redis,
_ => CacheSource.None,
};
}

View File

@@ -29,8 +29,8 @@ public static class PolicyEngineServiceCollectionExtensions
// Core compilation and evaluation services
services.TryAddSingleton<PolicyCompilationService>();
// Cache
services.TryAddSingleton<IPolicyEvaluationCache, InMemoryPolicyEvaluationCache>();
// Cache - uses IDistributedCacheFactory for transport flexibility
services.TryAddSingleton<IPolicyEvaluationCache, MessagingPolicyEvaluationCache>();
// Runtime evaluation
services.TryAddSingleton<PolicyRuntimeEvaluationService>();

View File

@@ -470,7 +470,7 @@ public sealed class PolicyGateEvaluator : IPolicyGateEvaluator
Name = "LatticeState",
Result = PolicyGateResultType.Warn,
Reason = $"{latticeState} may indicate false positive for affected",
Note = "Consider review: evidence suggests code may not be reachable"
Note = "Consider review: evidence suggests code may not be reachable (possible false positive)"
};
default:

View File

@@ -0,0 +1,180 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Messaging;
using StellaOps.Messaging.Abstractions;
using StellaOps.Policy.Engine.Options;
namespace StellaOps.Policy.Engine.ReachabilityFacts;
/// <summary>
/// Reachability facts overlay cache backed by <see cref="IDistributedCache{TValue}"/>.
/// Supports any transport (InMemory, Valkey, PostgreSQL) via factory injection.
/// </summary>
public sealed class MessagingReachabilityFactsOverlayCache : IReachabilityFactsOverlayCache
{
private readonly IDistributedCache<ReachabilityFact> _cache;
private readonly TimeProvider _timeProvider;
private readonly ILogger<MessagingReachabilityFactsOverlayCache> _logger;
private readonly TimeSpan _defaultTtl;
private long _totalRequests;
private long _cacheHits;
private long _cacheMisses;
public MessagingReachabilityFactsOverlayCache(
IDistributedCacheFactory cacheFactory,
ILogger<MessagingReachabilityFactsOverlayCache> logger,
TimeProvider timeProvider,
IOptions<PolicyEngineOptions> options)
{
ArgumentNullException.ThrowIfNull(cacheFactory);
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
var cacheOptions = options?.Value.ReachabilityCache ?? new ReachabilityFactsCacheOptions();
_defaultTtl = TimeSpan.FromMinutes(cacheOptions.DefaultTtlMinutes);
_cache = cacheFactory.Create<ReachabilityFact>(new CacheOptions
{
KeyPrefix = "rf:",
DefaultTtl = _defaultTtl,
});
_logger.LogInformation(
"Initialized MessagingReachabilityFactsOverlayCache with provider {Provider}, TTL {Ttl}",
_cache.ProviderName,
_defaultTtl);
}
public async Task<(ReachabilityFact? Fact, bool CacheHit)> GetAsync(
ReachabilityFactKey key,
CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref _totalRequests);
var cacheKey = key.ToCacheKey();
var result = await _cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false);
if (result.HasValue)
{
var fact = result.Value;
var now = _timeProvider.GetUtcNow();
// Check if entry is still valid
if (!fact.ExpiresAt.HasValue || fact.ExpiresAt.Value > now)
{
Interlocked.Increment(ref _cacheHits);
return (fact, true);
}
// Entry expired - remove it
await _cache.InvalidateAsync(cacheKey, cancellationToken).ConfigureAwait(false);
}
Interlocked.Increment(ref _cacheMisses);
return (null, false);
}
public async Task<ReachabilityFactsBatch> GetBatchAsync(
IReadOnlyList<ReachabilityFactKey> keys,
CancellationToken cancellationToken = default)
{
var found = new Dictionary<ReachabilityFactKey, ReachabilityFact>();
var notFound = new List<ReachabilityFactKey>();
var cacheHits = 0;
var cacheMisses = 0;
foreach (var key in keys)
{
var (fact, hit) = await GetAsync(key, cancellationToken).ConfigureAwait(false);
if (fact != null)
{
found[key] = fact;
cacheHits++;
}
else
{
notFound.Add(key);
cacheMisses++;
}
}
return new ReachabilityFactsBatch
{
Found = found,
NotFound = notFound,
CacheHits = cacheHits,
CacheMisses = cacheMisses,
};
}
public async Task SetAsync(
ReachabilityFactKey key,
ReachabilityFact fact,
CancellationToken cancellationToken = default)
{
var cacheKey = key.ToCacheKey();
var now = _timeProvider.GetUtcNow();
var ttl = fact.ExpiresAt.HasValue && fact.ExpiresAt.Value > now
? fact.ExpiresAt.Value - now
: _defaultTtl;
if (ttl <= TimeSpan.Zero)
{
return;
}
var options = new CacheEntryOptions { TimeToLive = ttl };
await _cache.SetAsync(cacheKey, fact, options, cancellationToken).ConfigureAwait(false);
}
public async Task SetBatchAsync(
IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact> facts,
CancellationToken cancellationToken = default)
{
var now = _timeProvider.GetUtcNow();
foreach (var (key, fact) in facts)
{
var cacheKey = key.ToCacheKey();
var ttl = fact.ExpiresAt.HasValue && fact.ExpiresAt.Value > now
? fact.ExpiresAt.Value - now
: _defaultTtl;
if (ttl <= TimeSpan.Zero)
{
continue;
}
var options = new CacheEntryOptions { TimeToLive = ttl };
await _cache.SetAsync(cacheKey, fact, options, cancellationToken).ConfigureAwait(false);
}
}
public async Task InvalidateAsync(ReachabilityFactKey key, CancellationToken cancellationToken = default)
{
var cacheKey = key.ToCacheKey();
await _cache.InvalidateAsync(cacheKey, cancellationToken).ConfigureAwait(false);
}
public async Task InvalidateTenantAsync(string tenantId, CancellationToken cancellationToken = default)
{
// Pattern: rf:<tenantId>:*
var pattern = $"{tenantId}:*";
var count = await _cache.InvalidateByPatternAsync(pattern, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Invalidated {Count} cache entries for tenant {TenantId}", count, tenantId);
}
public ReachabilityFactsCacheStats GetStats()
{
return new ReachabilityFactsCacheStats
{
TotalRequests = Interlocked.Read(ref _totalRequests),
CacheHits = Interlocked.Read(ref _cacheHits),
CacheMisses = Interlocked.Read(ref _cacheMisses),
ItemCount = 0, // Not available from IDistributedCache
EvictionCount = 0, // Not available from IDistributedCache
};
}
}

View File

@@ -4,4 +4,4 @@ This file mirrors sprint work for the Policy Engine module.
| Task ID | Sprint | Status | Notes |
| --- | --- | --- | --- |
| `POLICY-GATE-401-033` | `docs/implplan/SPRINT_0401_0001_0001_reachability_evidence_chain.md` | DOING | Gate `unreachable` reachability facts: missing evidence ref or low confidence => `under_investigation`; add tests and docs. |
| `POLICY-GATE-401-033` | `docs/implplan/SPRINT_0401_0001_0001_reachability_evidence_chain.md` | DONE (2025-12-13) | Implemented PolicyGateEvaluator (lattice/uncertainty/evidence completeness) and aligned tests/docs; see `src/Policy/StellaOps.Policy.Engine/Gates/PolicyGateEvaluator.cs` and `src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Gates/PolicyGateEvaluatorTests.cs`. |

View File

@@ -55,13 +55,20 @@ public sealed partial class TenantContextMiddleware
// Set tenant context for the request
tenantContextAccessor.TenantContext = validationResult.Context;
using (_logger.BeginScope(new Dictionary<string, object?>
try
{
["tenant_id"] = validationResult.Context?.TenantId,
["project_id"] = validationResult.Context?.ProjectId
}))
using (_logger.BeginScope(new Dictionary<string, object?>
{
["tenant_id"] = validationResult.Context?.TenantId,
["project_id"] = validationResult.Context?.ProjectId
}))
{
await _next(context);
}
}
finally
{
await _next(context);
tenantContextAccessor.TenantContext = null;
}
}