save development progress

This commit is contained in:
StellaOps Bot
2025-12-25 23:09:58 +02:00
parent d71853ad7e
commit aa70af062e
351 changed files with 37683 additions and 150156 deletions

View File

@@ -0,0 +1,218 @@
// -----------------------------------------------------------------------------
// AdvisoryCacheKeys.cs
// Sprint: SPRINT_8200_0013_0001_GW_valkey_advisory_cache
// Task: VCACHE-8200-004, VCACHE-8200-005, VCACHE-8200-006, VCACHE-8200-007, VCACHE-8200-008
// Description: Key schema for Concelier Valkey cache
// -----------------------------------------------------------------------------
using System.Text;
namespace StellaOps.Concelier.Cache.Valkey;
/// <summary>
/// Static class for generating Valkey cache keys for canonical advisories.
/// </summary>
/// <remarks>
/// Key Schema:
/// <code>
/// advisory:{merge_hash} → JSON(CanonicalAdvisory) - TTL based on interest_score
/// rank:hot → ZSET { merge_hash: interest_score } - max 10,000 entries
/// by:purl:{normalized_purl} → SET { merge_hash, ... } - TTL 24h
/// by:cve:{cve_id} → STRING merge_hash - TTL 24h
/// cache:stats:hits → INCR counter
/// cache:stats:misses → INCR counter
/// cache:warmup:last → STRING ISO8601 timestamp
/// </code>
/// </remarks>
public static class AdvisoryCacheKeys
{
/// <summary>
/// Default key prefix for all cache keys.
/// </summary>
public const string DefaultPrefix = "concelier:";
/// <summary>
/// Key for advisory by merge hash.
/// Format: {prefix}advisory:{mergeHash}
/// </summary>
public static string Advisory(string mergeHash, string prefix = DefaultPrefix)
=> $"{prefix}advisory:{mergeHash}";
/// <summary>
/// Key for the hot advisory sorted set.
/// Format: {prefix}rank:hot
/// </summary>
public static string HotSet(string prefix = DefaultPrefix)
=> $"{prefix}rank:hot";
/// <summary>
/// Key for PURL index set.
/// Format: {prefix}by:purl:{normalizedPurl}
/// </summary>
/// <param name="purl">The PURL (will be normalized).</param>
/// <param name="prefix">Key prefix.</param>
public static string ByPurl(string purl, string prefix = DefaultPrefix)
=> $"{prefix}by:purl:{NormalizePurl(purl)}";
/// <summary>
/// Key for CVE mapping.
/// Format: {prefix}by:cve:{cveId}
/// </summary>
/// <param name="cve">The CVE identifier (case-insensitive).</param>
/// <param name="prefix">Key prefix.</param>
public static string ByCve(string cve, string prefix = DefaultPrefix)
=> $"{prefix}by:cve:{cve.ToUpperInvariant()}";
/// <summary>
/// Key for cache hit counter.
/// Format: {prefix}cache:stats:hits
/// </summary>
public static string StatsHits(string prefix = DefaultPrefix)
=> $"{prefix}cache:stats:hits";
/// <summary>
/// Key for cache miss counter.
/// Format: {prefix}cache:stats:misses
/// </summary>
public static string StatsMisses(string prefix = DefaultPrefix)
=> $"{prefix}cache:stats:misses";
/// <summary>
/// Key for last warmup timestamp.
/// Format: {prefix}cache:warmup:last
/// </summary>
public static string WarmupLast(string prefix = DefaultPrefix)
=> $"{prefix}cache:warmup:last";
/// <summary>
/// Key for warmup lock (for distributed coordination).
/// Format: {prefix}cache:warmup:lock
/// </summary>
public static string WarmupLock(string prefix = DefaultPrefix)
=> $"{prefix}cache:warmup:lock";
/// <summary>
/// Key for total cached advisories gauge.
/// Format: {prefix}cache:stats:count
/// </summary>
public static string StatsCount(string prefix = DefaultPrefix)
=> $"{prefix}cache:stats:count";
/// <summary>
/// Pattern to match all advisory keys (for scanning/cleanup).
/// Format: {prefix}advisory:*
/// </summary>
public static string AdvisoryPattern(string prefix = DefaultPrefix)
=> $"{prefix}advisory:*";
/// <summary>
/// Pattern to match all PURL index keys (for scanning/cleanup).
/// Format: {prefix}by:purl:*
/// </summary>
public static string PurlIndexPattern(string prefix = DefaultPrefix)
=> $"{prefix}by:purl:*";
/// <summary>
/// Pattern to match all CVE mapping keys (for scanning/cleanup).
/// Format: {prefix}by:cve:*
/// </summary>
public static string CveMappingPattern(string prefix = DefaultPrefix)
=> $"{prefix}by:cve:*";
/// <summary>
/// Normalizes a PURL for use as a cache key.
/// </summary>
/// <param name="purl">The PURL to normalize.</param>
/// <returns>Normalized PURL safe for use in cache keys.</returns>
/// <remarks>
/// Normalization:
/// 1. Lowercase the entire PURL
/// 2. Replace special characters that may cause issues in keys
/// 3. Truncate very long PURLs to prevent oversized keys
/// </remarks>
public static string NormalizePurl(string purl)
{
if (string.IsNullOrWhiteSpace(purl))
{
return string.Empty;
}
// Normalize to lowercase
var normalized = purl.ToLowerInvariant();
// Replace characters that could cause issues in Redis keys
// Redis keys should avoid spaces and some special chars for simplicity
var sb = new StringBuilder(normalized.Length);
foreach (var c in normalized)
{
// Allow alphanumeric, standard PURL chars: : / @ . - _ %
if (char.IsLetterOrDigit(c) ||
c is ':' or '/' or '@' or '.' or '-' or '_' or '%')
{
sb.Append(c);
}
else
{
// Replace other chars with underscore
sb.Append('_');
}
}
// Truncate if too long (Redis keys can be up to 512MB, but we want reasonable sizes)
const int MaxKeyLength = 500;
if (sb.Length > MaxKeyLength)
{
return sb.ToString(0, MaxKeyLength);
}
return sb.ToString();
}
/// <summary>
/// Extracts the merge hash from an advisory key.
/// </summary>
/// <param name="key">The full advisory key.</param>
/// <param name="prefix">The key prefix used.</param>
/// <returns>The merge hash, or null if key doesn't match expected format.</returns>
public static string? ExtractMergeHash(string key, string prefix = DefaultPrefix)
{
var expectedStart = $"{prefix}advisory:";
if (key.StartsWith(expectedStart, StringComparison.Ordinal))
{
return key[expectedStart.Length..];
}
return null;
}
/// <summary>
/// Extracts the PURL from a PURL index key.
/// </summary>
/// <param name="key">The full PURL index key.</param>
/// <param name="prefix">The key prefix used.</param>
/// <returns>The normalized PURL, or null if key doesn't match expected format.</returns>
public static string? ExtractPurl(string key, string prefix = DefaultPrefix)
{
var expectedStart = $"{prefix}by:purl:";
if (key.StartsWith(expectedStart, StringComparison.Ordinal))
{
return key[expectedStart.Length..];
}
return null;
}
/// <summary>
/// Extracts the CVE from a CVE mapping key.
/// </summary>
/// <param name="key">The full CVE mapping key.</param>
/// <param name="prefix">The key prefix used.</param>
/// <returns>The CVE identifier, or null if key doesn't match expected format.</returns>
public static string? ExtractCve(string key, string prefix = DefaultPrefix)
{
var expectedStart = $"{prefix}by:cve:";
if (key.StartsWith(expectedStart, StringComparison.Ordinal))
{
return key[expectedStart.Length..];
}
return null;
}
}