save progress
This commit is contained in:
@@ -140,4 +140,16 @@ public sealed record CallgraphEdge
|
||||
/// </summary>
|
||||
[JsonPropertyName("provenance")]
|
||||
public string? Provenance { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gates detected on this edge.
|
||||
/// </summary>
|
||||
[JsonPropertyName("gates")]
|
||||
public IReadOnlyList<CallgraphGate>? Gates { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Combined gate multiplier in basis points (10000 = 100%).
|
||||
/// </summary>
|
||||
[JsonPropertyName("gateMultiplierBps")]
|
||||
public int GateMultiplierBps { get; init; } = 10000;
|
||||
}
|
||||
|
||||
52
src/Signals/StellaOps.Signals/Models/CallgraphGate.cs
Normal file
52
src/Signals/StellaOps.Signals/Models/CallgraphGate.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Signals.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A detected gate protecting a code path.
|
||||
/// </summary>
|
||||
public sealed record CallgraphGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of gate.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public CallgraphGateType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("detail")]
|
||||
public string Detail { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Symbol where gate was detected.
|
||||
/// </summary>
|
||||
[JsonPropertyName("guardSymbol")]
|
||||
public string GuardSymbol { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Source file (if available).
|
||||
/// </summary>
|
||||
[JsonPropertyName("sourceFile")]
|
||||
public string? SourceFile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Line number (if available).
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineNumber")]
|
||||
public int? LineNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score (0.0-1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public double Confidence { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Detection method used.
|
||||
/// </summary>
|
||||
[JsonPropertyName("detectionMethod")]
|
||||
public string DetectionMethod { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
16
src/Signals/StellaOps.Signals/Models/CallgraphGateType.cs
Normal file
16
src/Signals/StellaOps.Signals/Models/CallgraphGateType.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Signals.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Types of gates that can protect code paths.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum CallgraphGateType
|
||||
{
|
||||
AuthRequired,
|
||||
FeatureFlag,
|
||||
AdminOnly,
|
||||
NonDefaultConfig
|
||||
}
|
||||
|
||||
@@ -95,6 +95,16 @@ public sealed class ReachabilityEvidenceDocument
|
||||
public List<string> RuntimeHits { get; set; } = new();
|
||||
|
||||
public List<string>? BlockedEdges { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Combined gate multiplier in basis points (10000 = 100%).
|
||||
/// </summary>
|
||||
public int GateMultiplierBps { get; set; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Gates detected on the computed path to the target (if any).
|
||||
/// </summary>
|
||||
public List<CallgraphGate>? Gates { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ReachabilitySubject
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using StellaOps.Signals.Models;
|
||||
|
||||
namespace StellaOps.Signals.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Gate multiplier configuration in basis points (10000 = 100%).
|
||||
/// </summary>
|
||||
public sealed class SignalsGateMultiplierOptions
|
||||
{
|
||||
public int AuthRequiredMultiplierBps { get; set; } = 3000;
|
||||
|
||||
public int FeatureFlagMultiplierBps { get; set; } = 2000;
|
||||
|
||||
public int AdminOnlyMultiplierBps { get; set; } = 1500;
|
||||
|
||||
public int NonDefaultConfigMultiplierBps { get; set; } = 5000;
|
||||
|
||||
public int MinimumMultiplierBps { get; set; } = 500;
|
||||
|
||||
public int GetMultiplierBps(CallgraphGateType gateType) => gateType switch
|
||||
{
|
||||
CallgraphGateType.AuthRequired => AuthRequiredMultiplierBps,
|
||||
CallgraphGateType.FeatureFlag => FeatureFlagMultiplierBps,
|
||||
CallgraphGateType.AdminOnly => AdminOnlyMultiplierBps,
|
||||
CallgraphGateType.NonDefaultConfig => NonDefaultConfigMultiplierBps,
|
||||
_ => 10000
|
||||
};
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
EnsureBps(nameof(AuthRequiredMultiplierBps), AuthRequiredMultiplierBps);
|
||||
EnsureBps(nameof(FeatureFlagMultiplierBps), FeatureFlagMultiplierBps);
|
||||
EnsureBps(nameof(AdminOnlyMultiplierBps), AdminOnlyMultiplierBps);
|
||||
EnsureBps(nameof(NonDefaultConfigMultiplierBps), NonDefaultConfigMultiplierBps);
|
||||
EnsureBps(nameof(MinimumMultiplierBps), MinimumMultiplierBps);
|
||||
}
|
||||
|
||||
private static void EnsureBps(string name, int value)
|
||||
{
|
||||
if (value < 0 || value > 10000)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(name, value, "Value must be between 0 and 10000.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace StellaOps.Signals.Options;
|
||||
/// </summary>
|
||||
public sealed class SignalsScoringOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gate multipliers applied when reachability paths are protected by gates.
|
||||
/// </summary>
|
||||
public SignalsGateMultiplierOptions GateMultipliers { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Confidence assigned when a path exists from entry point to target.
|
||||
/// </summary>
|
||||
@@ -62,6 +67,8 @@ public sealed class SignalsScoringOptions
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
GateMultipliers.Validate();
|
||||
|
||||
EnsurePercent(nameof(ReachableConfidence), ReachableConfidence);
|
||||
EnsurePercent(nameof(UnreachableConfidence), UnreachableConfidence);
|
||||
EnsurePercent(nameof(RuntimeBonus), RuntimeBonus);
|
||||
|
||||
@@ -113,7 +113,9 @@ public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
SymbolDigest = GetString(edgeElement, "symbol_digest", "symbolDigest"),
|
||||
Candidates = GetStringArray(edgeElement, "candidates"),
|
||||
Confidence = GetNullableDouble(edgeElement, "confidence"),
|
||||
Evidence = GetStringArray(edgeElement, "evidence")
|
||||
Evidence = GetStringArray(edgeElement, "evidence"),
|
||||
Gates = ParseGates(edgeElement),
|
||||
GateMultiplierBps = GetNullableInt(edgeElement, "gate_multiplier_bps", "gateMultiplierBps") ?? 10000
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -212,7 +214,9 @@ public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
SymbolDigest = GetString(edgeElement, "symbol_digest", "symbolDigest"),
|
||||
Candidates = GetStringArray(edgeElement, "candidates"),
|
||||
Confidence = GetNullableDouble(edgeElement, "confidence"),
|
||||
Evidence = GetStringArray(edgeElement, "evidence")
|
||||
Evidence = GetStringArray(edgeElement, "evidence"),
|
||||
Gates = ParseGates(edgeElement),
|
||||
GateMultiplierBps = GetNullableInt(edgeElement, "gate_multiplier_bps", "gateMultiplierBps") ?? 10000
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -285,7 +289,9 @@ public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
SymbolDigest = GetString(edgeElement, "symbol_digest", "symbolDigest"),
|
||||
Candidates = GetStringArray(edgeElement, "candidates"),
|
||||
Confidence = GetNullableDouble(edgeElement, "confidence"),
|
||||
Evidence = GetStringArray(edgeElement, "evidence")
|
||||
Evidence = GetStringArray(edgeElement, "evidence"),
|
||||
Gates = ParseGates(edgeElement),
|
||||
GateMultiplierBps = GetNullableInt(edgeElement, "gate_multiplier_bps", "gateMultiplierBps") ?? 10000
|
||||
});
|
||||
}
|
||||
|
||||
@@ -434,4 +440,73 @@ public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
};
|
||||
}
|
||||
|
||||
private static int? GetNullableInt(JsonElement element, string name1, string? name2 = null)
|
||||
{
|
||||
if (element.TryGetProperty(name1, out var v1) && v1.ValueKind == JsonValueKind.Number && v1.TryGetInt32(out var i1))
|
||||
{
|
||||
return i1;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name2)
|
||||
&& element.TryGetProperty(name2!, out var v2)
|
||||
&& v2.ValueKind == JsonValueKind.Number
|
||||
&& v2.TryGetInt32(out var i2))
|
||||
{
|
||||
return i2;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CallgraphGate>? ParseGates(JsonElement edgeElement)
|
||||
{
|
||||
if (!edgeElement.TryGetProperty("gates", out var gatesEl) || gatesEl.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var gates = new List<CallgraphGate>(gatesEl.GetArrayLength());
|
||||
foreach (var gateEl in gatesEl.EnumerateArray())
|
||||
{
|
||||
if (gateEl.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var typeRaw = GetString(gateEl, "type");
|
||||
if (!TryParseGateType(typeRaw, out var gateType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
gates.Add(new CallgraphGate
|
||||
{
|
||||
Type = gateType,
|
||||
Detail = GetString(gateEl, "detail") ?? string.Empty,
|
||||
GuardSymbol = GetString(gateEl, "guard_symbol", "guardSymbol") ?? string.Empty,
|
||||
SourceFile = GetString(gateEl, "source_file", "sourceFile"),
|
||||
LineNumber = GetNullableInt(gateEl, "line_number", "lineNumber"),
|
||||
Confidence = GetNullableDouble(gateEl, "confidence") ?? 1.0,
|
||||
DetectionMethod = GetString(gateEl, "detection_method", "detectionMethod") ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
return gates.Count == 0 ? null : gates;
|
||||
}
|
||||
|
||||
private static bool TryParseGateType(string? raw, out CallgraphGateType gateType)
|
||||
{
|
||||
gateType = default;
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = raw.Trim()
|
||||
.Replace("_", string.Empty, StringComparison.Ordinal)
|
||||
.Replace("-", string.Empty, StringComparison.Ordinal);
|
||||
|
||||
return Enum.TryParse(normalized, ignoreCase: true, out gateType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -127,6 +127,19 @@ internal sealed class InMemoryCallgraphRepository : ICallgraphRepository
|
||||
Weight = source.Weight,
|
||||
Offset = source.Offset,
|
||||
IsResolved = source.IsResolved,
|
||||
Provenance = source.Provenance
|
||||
Provenance = source.Provenance,
|
||||
GateMultiplierBps = source.GateMultiplierBps,
|
||||
Gates = source.Gates?.Select(CloneGate).ToList()
|
||||
};
|
||||
|
||||
private static CallgraphGate CloneGate(CallgraphGate source) => new()
|
||||
{
|
||||
Type = source.Type,
|
||||
Detail = source.Detail,
|
||||
GuardSymbol = source.GuardSymbol,
|
||||
SourceFile = source.SourceFile,
|
||||
LineNumber = source.LineNumber,
|
||||
Confidence = source.Confidence,
|
||||
DetectionMethod = source.DetectionMethod
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,14 +119,30 @@ internal sealed class InMemoryReachabilityFactRepository : IReachabilityFactRepo
|
||||
Reachable = source.Reachable,
|
||||
Confidence = source.Confidence,
|
||||
Bucket = source.Bucket,
|
||||
LatticeState = source.LatticeState,
|
||||
PreviousLatticeState = source.PreviousLatticeState,
|
||||
Weight = source.Weight,
|
||||
Score = source.Score,
|
||||
Path = source.Path.ToList(),
|
||||
Evidence = new ReachabilityEvidenceDocument
|
||||
{
|
||||
RuntimeHits = source.Evidence.RuntimeHits.ToList(),
|
||||
BlockedEdges = source.Evidence.BlockedEdges?.ToList()
|
||||
}
|
||||
BlockedEdges = source.Evidence.BlockedEdges?.ToList(),
|
||||
GateMultiplierBps = source.Evidence.GateMultiplierBps,
|
||||
Gates = source.Evidence.Gates?.Select(CloneGate).ToList()
|
||||
},
|
||||
LatticeTransitionAt = source.LatticeTransitionAt
|
||||
};
|
||||
|
||||
private static CallgraphGate CloneGate(CallgraphGate source) => new()
|
||||
{
|
||||
Type = source.Type,
|
||||
Detail = source.Detail,
|
||||
GuardSymbol = source.GuardSymbol,
|
||||
SourceFile = source.SourceFile,
|
||||
LineNumber = source.LineNumber,
|
||||
Confidence = source.Confidence,
|
||||
DetectionMethod = source.DetectionMethod
|
||||
};
|
||||
|
||||
private static RuntimeFactDocument CloneRuntime(RuntimeFactDocument source) => new()
|
||||
|
||||
@@ -272,6 +272,8 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
.Append(edge.Offset?.ToString(CultureInfo.InvariantCulture) ?? string.Empty).Append('|')
|
||||
.Append(edge.IsResolved).Append('|')
|
||||
.Append(edge.Provenance).Append('|')
|
||||
.Append(edge.GateMultiplierBps.ToString(CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(JoinGates(edge.Gates)).Append('|')
|
||||
.Append(edge.Purl).Append('|')
|
||||
.Append(edge.SymbolDigest).Append('|')
|
||||
.Append(edge.Confidence?.ToString("G17", CultureInfo.InvariantCulture) ?? string.Empty).Append('|')
|
||||
@@ -330,6 +332,49 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
return ordered.ToString();
|
||||
}
|
||||
|
||||
private static string JoinGates(IReadOnlyList<CallgraphGate>? gates)
|
||||
{
|
||||
if (gates is null || gates.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var ordered = gates
|
||||
.Where(g => g is not null)
|
||||
.Select(g => g with
|
||||
{
|
||||
GuardSymbol = g.GuardSymbol?.Trim() ?? string.Empty,
|
||||
Detail = g.Detail?.Trim() ?? string.Empty,
|
||||
DetectionMethod = g.DetectionMethod?.Trim() ?? string.Empty,
|
||||
SourceFile = string.IsNullOrWhiteSpace(g.SourceFile) ? null : g.SourceFile.Trim(),
|
||||
LineNumber = g.LineNumber is > 0 ? g.LineNumber : null,
|
||||
Confidence = double.IsNaN(g.Confidence) ? 0.0 : Math.Clamp(g.Confidence, 0.0, 1.0)
|
||||
})
|
||||
.OrderBy(g => g.Type)
|
||||
.ThenBy(g => g.GuardSymbol, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.DetectionMethod, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.Detail, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.SourceFile, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.LineNumber ?? 0)
|
||||
.ToList();
|
||||
|
||||
var builder = new StringBuilder();
|
||||
foreach (var gate in ordered)
|
||||
{
|
||||
builder
|
||||
.Append(gate.Type).Append(':')
|
||||
.Append(gate.GuardSymbol).Append(':')
|
||||
.Append(gate.DetectionMethod).Append(':')
|
||||
.Append(gate.Confidence.ToString("G17", CultureInfo.InvariantCulture)).Append(':')
|
||||
.Append(gate.SourceFile).Append(':')
|
||||
.Append(gate.LineNumber?.ToString(CultureInfo.InvariantCulture) ?? string.Empty).Append(':')
|
||||
.Append(gate.Detail)
|
||||
.Append(';');
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -108,6 +108,8 @@ internal sealed class CallgraphNormalizationService : ICallgraphNormalizationSer
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalizedGates = NormalizeGates(edge.Gates);
|
||||
|
||||
list.Add(edge with
|
||||
{
|
||||
SourceId = source,
|
||||
@@ -117,7 +119,9 @@ internal sealed class CallgraphNormalizationService : ICallgraphNormalizationSer
|
||||
SymbolDigest = NormalizeDigest(edge.SymbolDigest),
|
||||
Confidence = ClampConfidence(edge.Confidence),
|
||||
Candidates = NormalizeList(edge.Candidates),
|
||||
Evidence = NormalizeList(edge.Evidence)
|
||||
Evidence = NormalizeList(edge.Evidence),
|
||||
Gates = normalizedGates,
|
||||
GateMultiplierBps = ClampGateMultiplierBps(edge.GateMultiplierBps)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,6 +131,47 @@ internal sealed class CallgraphNormalizationService : ICallgraphNormalizationSer
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CallgraphGate>? NormalizeGates(IReadOnlyList<CallgraphGate>? gates)
|
||||
{
|
||||
if (gates is null || gates.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var unique = new Dictionary<(CallgraphGateType Type, string GuardSymbol), CallgraphGate>();
|
||||
foreach (var gate in gates.Where(g => g is not null))
|
||||
{
|
||||
var guardSymbol = gate.GuardSymbol?.Trim() ?? string.Empty;
|
||||
var normalized = gate with
|
||||
{
|
||||
GuardSymbol = guardSymbol,
|
||||
Detail = gate.Detail?.Trim() ?? string.Empty,
|
||||
DetectionMethod = gate.DetectionMethod?.Trim() ?? string.Empty,
|
||||
SourceFile = string.IsNullOrWhiteSpace(gate.SourceFile) ? null : gate.SourceFile.Trim(),
|
||||
LineNumber = gate.LineNumber is > 0 ? gate.LineNumber : null,
|
||||
Confidence = double.IsNaN(gate.Confidence) ? 0.0 : Math.Clamp(gate.Confidence, 0.0, 1.0)
|
||||
};
|
||||
|
||||
var key = (normalized.Type, guardSymbol);
|
||||
if (!unique.TryGetValue(key, out var existing) || normalized.Confidence > existing.Confidence)
|
||||
{
|
||||
unique[key] = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return unique.Values
|
||||
.OrderBy(g => g.Type)
|
||||
.ThenBy(g => g.GuardSymbol, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.DetectionMethod, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.Detail, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.SourceFile, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.LineNumber ?? 0)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static int ClampGateMultiplierBps(int multiplierBps)
|
||||
=> Math.Clamp(multiplierBps, 0, 10000);
|
||||
|
||||
private static IReadOnlyList<CallgraphRoot> NormalizeRoots(IReadOnlyList<CallgraphRoot>? roots)
|
||||
{
|
||||
var list = new List<CallgraphRoot>();
|
||||
|
||||
@@ -70,7 +70,35 @@ internal static class ReachabilityFactDigestCalculator
|
||||
Score: state.Score,
|
||||
Path: NormalizeList(state.Path),
|
||||
RuntimeHits: NormalizeList(state.Evidence?.RuntimeHits),
|
||||
BlockedEdges: NormalizeList(state.Evidence?.BlockedEdges)))
|
||||
BlockedEdges: NormalizeList(state.Evidence?.BlockedEdges),
|
||||
GateMultiplierBps: Math.Clamp(state.Evidence?.GateMultiplierBps ?? 10000, 0, 10000),
|
||||
Gates: NormalizeGates(state.Evidence?.Gates)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<CanonicalGate> NormalizeGates(IEnumerable<CallgraphGate>? gates)
|
||||
{
|
||||
if (gates is null)
|
||||
{
|
||||
return new List<CanonicalGate>();
|
||||
}
|
||||
|
||||
return gates
|
||||
.Where(g => g is not null)
|
||||
.Select(g => new CanonicalGate(
|
||||
Type: g.Type.ToString(),
|
||||
GuardSymbol: g.GuardSymbol?.Trim() ?? string.Empty,
|
||||
DetectionMethod: g.DetectionMethod?.Trim() ?? string.Empty,
|
||||
Confidence: double.IsNaN(g.Confidence) ? 0.0 : Math.Clamp(g.Confidence, 0.0, 1.0),
|
||||
SourceFile: string.IsNullOrWhiteSpace(g.SourceFile) ? null : g.SourceFile.Trim(),
|
||||
LineNumber: g.LineNumber is > 0 ? g.LineNumber : null,
|
||||
Detail: g.Detail?.Trim() ?? string.Empty))
|
||||
.OrderBy(g => g.Type, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.GuardSymbol, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.DetectionMethod, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.SourceFile, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.LineNumber ?? 0)
|
||||
.ThenBy(g => g.Detail, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -192,7 +220,18 @@ internal static class ReachabilityFactDigestCalculator
|
||||
double Score,
|
||||
List<string> Path,
|
||||
List<string> RuntimeHits,
|
||||
List<string> BlockedEdges);
|
||||
List<string> BlockedEdges,
|
||||
int GateMultiplierBps,
|
||||
List<CanonicalGate> Gates);
|
||||
|
||||
private sealed record CanonicalGate(
|
||||
string Type,
|
||||
string GuardSymbol,
|
||||
string DetectionMethod,
|
||||
double Confidence,
|
||||
string? SourceFile,
|
||||
int? LineNumber,
|
||||
string Detail);
|
||||
|
||||
private sealed record CanonicalRuntimeFact(
|
||||
string SymbolId,
|
||||
|
||||
@@ -68,6 +68,7 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
? list
|
||||
: Array.Empty<ReachabilityBlockedEdge>();
|
||||
var graph = BuildGraph(callgraph, blockedEdges);
|
||||
var edgeGateMap = BuildEdgeGateMap(callgraph, blockedEdges);
|
||||
var entryPoints = NormalizeEntryPoints(request.EntryPoints, graph.Nodes, graph.Inbound);
|
||||
var targets = request.Targets.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t.Trim()).Distinct(StringComparer.Ordinal).ToList();
|
||||
if (targets.Count == 0)
|
||||
@@ -101,6 +102,7 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
var path = FindPath(entryPoints, target, graph.Adjacency);
|
||||
var reachable = path is not null;
|
||||
var runtimeEvidence = runtimeHits.Where(hit => path?.Contains(hit, StringComparer.Ordinal) == true).ToList();
|
||||
var (pathGateMultiplierBps, pathGates) = ComputePathGateMultiplier(path, edgeGateMap);
|
||||
|
||||
var (bucket, weight, confidence) = ComputeScores(
|
||||
reachable,
|
||||
@@ -109,7 +111,7 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
path,
|
||||
runtimeEvidence.Count);
|
||||
|
||||
var score = confidence * weight;
|
||||
var score = confidence * weight * pathGateMultiplierBps / 10000.0;
|
||||
|
||||
runtimeEvidence = runtimeEvidence.OrderBy(hit => hit, StringComparer.Ordinal).ToList();
|
||||
|
||||
@@ -139,7 +141,9 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
BlockedEdges = request.BlockedEdges?
|
||||
.Select(edge => $"{edge.From} -> {edge.To}")
|
||||
.OrderBy(edge => edge, StringComparer.Ordinal)
|
||||
.ToList()
|
||||
.ToList(),
|
||||
GateMultiplierBps = pathGateMultiplierBps,
|
||||
Gates = pathGates
|
||||
},
|
||||
LatticeTransitionAt = previousLatticeState != latticeState.ToCode() ? computedAt : existingState?.LatticeTransitionAt
|
||||
});
|
||||
@@ -387,6 +391,177 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
return new ReachabilityGraph(nodes, adjacency, inbound);
|
||||
}
|
||||
|
||||
private Dictionary<(string From, string To), EdgeGateInfo> BuildEdgeGateMap(CallgraphDocument document, IEnumerable<ReachabilityBlockedEdge> blockedEdges)
|
||||
{
|
||||
var blocked = new HashSet<(string From, string To)>(new ReachabilityBlockedEdgeComparer());
|
||||
foreach (var blockedEdge in blockedEdges)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(blockedEdge.From) && !string.IsNullOrWhiteSpace(blockedEdge.To))
|
||||
{
|
||||
blocked.Add((blockedEdge.From.Trim(), blockedEdge.To.Trim()));
|
||||
}
|
||||
}
|
||||
|
||||
var map = new Dictionary<(string From, string To), EdgeGateInfo>();
|
||||
foreach (var edge in document.Edges)
|
||||
{
|
||||
if (blocked.Contains((edge.SourceId, edge.TargetId)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var from = edge.SourceId?.Trim();
|
||||
var to = edge.TargetId?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalizedGates = NormalizeGates(edge.Gates);
|
||||
var multiplierBps = ComputeGateMultiplierBps(normalizedGates);
|
||||
var key = (from, to);
|
||||
|
||||
if (!map.TryGetValue(key, out var existing))
|
||||
{
|
||||
map[key] = new EdgeGateInfo(multiplierBps, normalizedGates);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (multiplierBps > existing.GateMultiplierBps)
|
||||
{
|
||||
map[key] = new EdgeGateInfo(multiplierBps, normalizedGates);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (multiplierBps == existing.GateMultiplierBps
|
||||
&& (normalizedGates?.Count ?? 0) < (existing.Gates?.Count ?? 0))
|
||||
{
|
||||
map[key] = new EdgeGateInfo(multiplierBps, normalizedGates);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private (int GateMultiplierBps, List<CallgraphGate>? Gates) ComputePathGateMultiplier(
|
||||
List<string>? path,
|
||||
IReadOnlyDictionary<(string From, string To), EdgeGateInfo> edgeGateMap)
|
||||
{
|
||||
if (path is null || path.Count < 2)
|
||||
{
|
||||
return (10000, null);
|
||||
}
|
||||
|
||||
var gatesByKey = new Dictionary<(CallgraphGateType Type, string GuardSymbol), CallgraphGate>();
|
||||
for (var i = 0; i < path.Count - 1; i++)
|
||||
{
|
||||
var from = path[i];
|
||||
var to = path[i + 1];
|
||||
if (!edgeGateMap.TryGetValue((from, to), out var edgeInfo) || edgeInfo.Gates is not { Count: > 0 })
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var gate in edgeInfo.Gates)
|
||||
{
|
||||
var guardSymbol = gate.GuardSymbol?.Trim() ?? string.Empty;
|
||||
var normalized = gate with
|
||||
{
|
||||
GuardSymbol = guardSymbol,
|
||||
Detail = gate.Detail?.Trim() ?? string.Empty,
|
||||
DetectionMethod = gate.DetectionMethod?.Trim() ?? string.Empty,
|
||||
SourceFile = string.IsNullOrWhiteSpace(gate.SourceFile) ? null : gate.SourceFile.Trim(),
|
||||
LineNumber = gate.LineNumber is > 0 ? gate.LineNumber : null,
|
||||
Confidence = double.IsNaN(gate.Confidence) ? 0.0 : Math.Clamp(gate.Confidence, 0.0, 1.0)
|
||||
};
|
||||
|
||||
var key = (normalized.Type, guardSymbol);
|
||||
if (!gatesByKey.TryGetValue(key, out var existing) || normalized.Confidence > existing.Confidence)
|
||||
{
|
||||
gatesByKey[key] = normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (gatesByKey.Count == 0)
|
||||
{
|
||||
return (10000, null);
|
||||
}
|
||||
|
||||
var gates = gatesByKey.Values
|
||||
.OrderBy(g => g.Type)
|
||||
.ThenBy(g => g.GuardSymbol, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.DetectionMethod, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.Detail, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.SourceFile, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.LineNumber ?? 0)
|
||||
.ToList();
|
||||
|
||||
return (ComputeGateMultiplierBps(gates), gates);
|
||||
}
|
||||
|
||||
private int ComputeGateMultiplierBps(IReadOnlyList<CallgraphGate>? gates)
|
||||
{
|
||||
if (gates is null || gates.Count == 0)
|
||||
{
|
||||
return 10000;
|
||||
}
|
||||
|
||||
var gateTypes = gates
|
||||
.Select(g => g.Type)
|
||||
.Distinct()
|
||||
.OrderBy(t => t)
|
||||
.ToList();
|
||||
|
||||
double multiplierBps = 10000.0;
|
||||
foreach (var gateType in gateTypes)
|
||||
{
|
||||
multiplierBps = multiplierBps * scoringOptions.GateMultipliers.GetMultiplierBps(gateType) / 10000.0;
|
||||
}
|
||||
|
||||
var result = (int)Math.Round(multiplierBps);
|
||||
result = Math.Clamp(result, 0, 10000);
|
||||
return Math.Max(result, scoringOptions.GateMultipliers.MinimumMultiplierBps);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CallgraphGate>? NormalizeGates(IReadOnlyList<CallgraphGate>? gates)
|
||||
{
|
||||
if (gates is null || gates.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var unique = new Dictionary<(CallgraphGateType Type, string GuardSymbol), CallgraphGate>();
|
||||
foreach (var gate in gates.Where(g => g is not null))
|
||||
{
|
||||
var guardSymbol = gate.GuardSymbol?.Trim() ?? string.Empty;
|
||||
var normalized = gate with
|
||||
{
|
||||
GuardSymbol = guardSymbol,
|
||||
Detail = gate.Detail?.Trim() ?? string.Empty,
|
||||
DetectionMethod = gate.DetectionMethod?.Trim() ?? string.Empty,
|
||||
SourceFile = string.IsNullOrWhiteSpace(gate.SourceFile) ? null : gate.SourceFile.Trim(),
|
||||
LineNumber = gate.LineNumber is > 0 ? gate.LineNumber : null,
|
||||
Confidence = double.IsNaN(gate.Confidence) ? 0.0 : Math.Clamp(gate.Confidence, 0.0, 1.0)
|
||||
};
|
||||
|
||||
var key = (normalized.Type, guardSymbol);
|
||||
if (!unique.TryGetValue(key, out var existing) || normalized.Confidence > existing.Confidence)
|
||||
{
|
||||
unique[key] = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return unique.Values
|
||||
.OrderBy(g => g.Type)
|
||||
.ThenBy(g => g.GuardSymbol, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.DetectionMethod, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.Detail, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.SourceFile, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.LineNumber ?? 0)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<string> NormalizeEntryPoints(IEnumerable<string> requestedEntries, HashSet<string> nodes, Dictionary<string, HashSet<string>> inbound)
|
||||
{
|
||||
var entries = requestedEntries?
|
||||
@@ -516,6 +691,8 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
Dictionary<string, HashSet<string>> Adjacency,
|
||||
Dictionary<string, HashSet<string>> Inbound);
|
||||
|
||||
private sealed record EdgeGateInfo(int GateMultiplierBps, IReadOnlyList<CallgraphGate>? Gates);
|
||||
|
||||
private sealed class ReachabilityBlockedEdgeComparer : IEqualityComparer<(string From, string To)>
|
||||
{
|
||||
public bool Equals((string From, string To) x, (string From, string To) y)
|
||||
|
||||
@@ -9,3 +9,6 @@ This file mirrors sprint work for the Signals module.
|
||||
| `UNCERTAINTY-SCORER-401-025` | `docs/implplan/SPRINT_0401_0001_0001_reachability_evidence_chain.md` | DONE (2025-12-13) | Reachability risk score now uses configurable entropy weights and is aligned with `UncertaintyDocument.RiskScore`; tests cover tier/entropy scoring. |
|
||||
| `UNKNOWNS-DECAY-3601-001` | `docs/implplan/SPRINT_3601_0001_0001_unknowns_decay_algorithm.md` | DONE (2025-12-17) | Implemented decay worker/service, signal refresh hook, and deterministic unit/integration tests. |
|
||||
| `TRI-MASTER-0003` | `docs/implplan/SPRINT_3600_0001_0001_triage_unknowns_master.md` | DONE (2025-12-17) | Synced Signals AGENTS with Unknowns scoring/decay contracts and configuration sections. |
|
||||
| `GATE-3405-011` | `docs/implplan/SPRINT_3405_0001_0001_gate_multipliers.md` | DONE (2025-12-18) | Applied gate multipliers in `ReachabilityScoringService` using path gate evidence from callgraph edges. |
|
||||
| `GATE-3405-012` | `docs/implplan/SPRINT_3405_0001_0001_gate_multipliers.md` | DONE (2025-12-18) | Extended reachability fact evidence contract + digest to include `GateMultiplierBps` and `Gates`. |
|
||||
| `GATE-3405-016` | `docs/implplan/SPRINT_3405_0001_0001_gate_multipliers.md` | DONE (2025-12-18) | Added deterministic parser/normalizer/scoring coverage for gate propagation + multiplier effect. |
|
||||
|
||||
Reference in New Issue
Block a user