diff --git a/.agents/settings.json b/.agents/settings.json new file mode 100644 index 000000000..6523530a8 --- /dev/null +++ b/.agents/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "railway@claude-plugins-official": true + } +} diff --git a/evals/Qyl.Observability.Evaluation.Tests/CardinalitySafetyEvaluatorTests.cs b/evals/Qyl.Observability.Evaluation.Tests/CardinalitySafetyEvaluatorTests.cs new file mode 100644 index 000000000..534afd23d --- /dev/null +++ b/evals/Qyl.Observability.Evaluation.Tests/CardinalitySafetyEvaluatorTests.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Evaluation; +using Qyl.Observability.Evaluation.Evaluators; + +namespace Qyl.Observability.Evaluation.Tests; + +public sealed class CardinalitySafetyEvaluatorTests +{ + [Fact] + public void Analyze_PassesSafeBoundedAttributes() + { + var record = EvaluatorTestData.Record( + telemetry: [EvaluatorTestData.Span("span", "trace-1", "span-1", attributesJson: """{"http.route":"/checkout","error.type":"TimeoutException"}""")]); + + AnalysisResult result = CardinalitySafetyEvaluator.Analyze(record); + + Assert.True(result.Passed, result.Reason); + } + + [Fact] + public void Analyze_FailsBlockedAttributeKey() + { + var record = EvaluatorTestData.Record( + telemetry: [EvaluatorTestData.Span("span", "trace-1", "span-1", attributesJson: """{"qyl.prompt.raw":"diagnose alice@example.com"}""")]); + + AnalysisResult result = CardinalitySafetyEvaluator.Analyze(record); + + Assert.False(result.Passed); + Assert.Contains("span:qyl.prompt.raw", result.Reason, StringComparison.Ordinal); + } + + [Theory] + [InlineData("""{"user.id":"alice@example.com"}""")] + [InlineData("""{"auth.header":"Bearer token"}""")] + [InlineData("""{"openai.key":"sk-test"}""")] + public void Analyze_FailsSensitiveAttributeValues(string attributesJson) + { + var record = EvaluatorTestData.Record( + telemetry: [EvaluatorTestData.Span("span", "trace-1", "span-1", attributesJson: attributesJson)]); + + AnalysisResult result = CardinalitySafetyEvaluator.Analyze(record); + + Assert.False(result.Passed); + } + + [Fact] + public async Task EvaluateAsync_ReturnsFailedMetricForUnsafeAttribute() + { + var record = EvaluatorTestData.Record( + telemetry: [EvaluatorTestData.Span("span", "trace-1", "span-1", attributesJson: """{"message.raw":"secret"}""")]); + + EvaluationResult result = await new CardinalitySafetyEvaluator(record).EvaluateAsync( + [], + new ChatResponse(new ChatMessage(ChatRole.Assistant, "")), + cancellationToken: TestContext.Current.CancellationToken); + + BooleanMetric metric = result.Get("qyl.cardinality.safety"); + Assert.False(metric.Value); + Assert.True(metric.Interpretation?.Failed); + } +} diff --git a/evals/Qyl.Observability.Evaluation.Tests/EvaluatorTestData.cs b/evals/Qyl.Observability.Evaluation.Tests/EvaluatorTestData.cs new file mode 100644 index 000000000..b409318f5 --- /dev/null +++ b/evals/Qyl.Observability.Evaluation.Tests/EvaluatorTestData.cs @@ -0,0 +1,100 @@ +using System.Text.Json; +using Qyl.Observability.Evaluation.Models; + +namespace Qyl.Observability.Evaluation.Tests; + +internal static class EvaluatorTestData +{ + public static ObservabilityEvaluationRecord Record( + string finalResponse = "", + IReadOnlyList? toolCalls = null, + IReadOnlyList? expectedToolCalls = null, + IReadOnlyList? telemetry = null, + IReadOnlyList? requiredEvidenceIds = null, + IReadOnlyList? forbiddenClaims = null, + IReadOnlyList? expectedFailedMetrics = null, + bool shouldPass = true) + { + return new ObservabilityEvaluationRecord + { + Id = "record", + Source = "tests", + Scenario = "scenario", + Agent = new AgentInfo + { + Name = "QylIncidentAgent", + ModelProvider = "fixture", + ModelName = "deterministic", + Instructions = "Use qyl observability tools.", + Tools = ["qyl.query.traces", "qyl.query.metrics", "qyl.query.logs"] + }, + UserInput = "diagnose incident", + ToolCalls = toolCalls ?? [], + FinalResponse = finalResponse, + Telemetry = telemetry ?? [], + RequiredEvidenceIds = requiredEvidenceIds ?? [], + ForbiddenClaims = forbiddenClaims ?? [], + ExpectedToolCalls = expectedToolCalls ?? [], + ExpectedFailedMetrics = expectedFailedMetrics ?? [], + ShouldPass = shouldPass + }; + } + + public static ToolCallRecord ToolCall(string name, string argumentsJson = "{}") + { + return new ToolCallRecord + { + Name = name, + Arguments = ParseObject(argumentsJson), + ResultSummary = "ok" + }; + } + + public static ExpectedToolCallRecord ExpectedToolCall(string name, string argumentsJson = "{}") + { + return new ExpectedToolCallRecord + { + Name = name, + Arguments = ParseObject(argumentsJson) + }; + } + + public static TelemetryEvidenceRecord Span( + string id, + string? traceId, + string? spanId, + string? parentSpanId = null, + string attributesJson = "{}") + { + return new TelemetryEvidenceRecord + { + Id = id, + SignalType = "span", + ServiceName = "checkout-api", + Operation = "POST /checkout", + TraceId = traceId, + SpanId = spanId, + ParentSpanId = parentSpanId, + Attributes = ParseObject(attributesJson) + }; + } + + public static TelemetryEvidenceRecord Metric(string id) + { + return new TelemetryEvidenceRecord + { + Id = id, + SignalType = "metric", + ServiceName = "checkout-api", + Operation = "http.server.duration", + Attributes = ParseObject("""{"metric":"http.server.duration"}""") + }; + } + + private static IReadOnlyDictionary ParseObject(string json) + { + using JsonDocument document = JsonDocument.Parse(json); + return document.RootElement.EnumerateObject() + .ToDictionary(static property => property.Name, static property => property.Value.Clone(), StringComparer.Ordinal); + } +} diff --git a/evals/Qyl.Observability.Evaluation.Tests/Qyl.Observability.Evaluation.Tests.csproj b/evals/Qyl.Observability.Evaluation.Tests/Qyl.Observability.Evaluation.Tests.csproj new file mode 100644 index 000000000..dfde66d0d --- /dev/null +++ b/evals/Qyl.Observability.Evaluation.Tests/Qyl.Observability.Evaluation.Tests.csproj @@ -0,0 +1,14 @@ + + + + Qyl.Observability.Evaluation.Tests + Exe + false + $(NoWarn);CS1591;CA1707 + + + + + + + diff --git a/evals/Qyl.Observability.Evaluation.Tests/RunnerAndLoaderTests.cs b/evals/Qyl.Observability.Evaluation.Tests/RunnerAndLoaderTests.cs new file mode 100644 index 000000000..4a31d8eaf --- /dev/null +++ b/evals/Qyl.Observability.Evaluation.Tests/RunnerAndLoaderTests.cs @@ -0,0 +1,91 @@ +using Microsoft.Extensions.AI.Evaluation; +using Qyl.Observability.Evaluation; +using Qyl.Observability.Evaluation.Evaluators; + +namespace Qyl.Observability.Evaluation.Tests; + +public sealed class RunnerAndLoaderTests +{ + [Fact] + public void ScenarioRunResult_FailsWhenPassRecordDeclaresExpectedFailures() + { + var record = EvaluatorTestData.Record(expectedFailedMetrics: ["qyl.trace.correlation"], shouldPass: true); + + ScenarioRunResult result = ScenarioRunResult.Create(record, [PassedMetric("qyl.trace.correlation")]); + + Assert.False(result.Passed); + Assert.Contains("pass record declares expected failed metrics", result.Mismatches); + } + + [Fact] + public void ScenarioRunResult_FailsWhenFailRecordDeclaresNoExpectedFailures() + { + var record = EvaluatorTestData.Record(shouldPass: false); + + ScenarioRunResult result = ScenarioRunResult.Create(record, [FailedMetric("qyl.trace.correlation")]); + + Assert.False(result.Passed); + Assert.Contains("fail record declares no expected failed metrics", result.Mismatches); + } + + [Fact] + public void ScenarioRunResult_FailsWhenActualFailedMetricsDifferFromExpected() + { + var record = EvaluatorTestData.Record(expectedFailedMetrics: ["qyl.tool.call.accuracy"], shouldPass: false); + + ScenarioRunResult result = ScenarioRunResult.Create(record, [FailedMetric("qyl.trace.correlation")]); + + Assert.False(result.Passed); + Assert.Contains("failed metrics [qyl.trace.correlation] != expected [qyl.tool.call.accuracy]", result.Mismatches); + } + + [Fact] + public async Task EvaluationRunner_MatchesExpectedFailedMetrics() + { + var records = new[] + { + EvaluatorTestData.Record( + finalResponse: "Evidence ev-metric supports this.", + telemetry: [EvaluatorTestData.Metric("ev-metric")], + requiredEvidenceIds: ["ev-metric"], + shouldPass: true), + EvaluatorTestData.Record( + finalResponse: "No citation.", + requiredEvidenceIds: ["ev-missing"], + expectedFailedMetrics: ["qyl.telemetry.evidence"], + shouldPass: false) + }; + + IReadOnlyList results = await EvaluationRunner.RunAsync(records, TestContext.Current.CancellationToken); + + Assert.All(results, static result => Assert.True(result.Passed, string.Join("; ", result.Mismatches))); + } + + [Fact] + public void ScenarioLoader_LoadsJsonlAndSkipsBlankLines() + { + string path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.jsonl"); + File.WriteAllText(path, """ +{"id":"fixture","source":"tests","scenario":"load","agent":{"name":"agent","modelProvider":"fixture","modelName":"model","instructions":"instructions","tools":[]},"userInput":"input","toolCalls":[],"finalResponse":"response","telemetry":[],"requiredEvidenceIds":[],"forbiddenClaims":[],"expectedToolCalls":[],"expectedFailedMetrics":[],"shouldPass":true} + +"""); + + try + { + var records = ScenarioLoader.LoadJsonl(path); + + Assert.Single(records); + Assert.Equal("fixture", records[0].Id); + } + finally + { + File.Delete(path); + } + } + + private static BooleanMetric PassedMetric(string name) + => EvaluationMetricFactory.CreateBoolean(name, true, "passed"); + + private static BooleanMetric FailedMetric(string name) + => EvaluationMetricFactory.CreateBoolean(name, false, "failed"); +} diff --git a/evals/Qyl.Observability.Evaluation.Tests/TelemetryEvidenceEvaluatorTests.cs b/evals/Qyl.Observability.Evaluation.Tests/TelemetryEvidenceEvaluatorTests.cs new file mode 100644 index 000000000..ba9ac494f --- /dev/null +++ b/evals/Qyl.Observability.Evaluation.Tests/TelemetryEvidenceEvaluatorTests.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Evaluation; +using Qyl.Observability.Evaluation.Evaluators; + +namespace Qyl.Observability.Evaluation.Tests; + +public sealed class TelemetryEvidenceEvaluatorTests +{ + [Fact] + public void Analyze_PassesWhenEvidenceExistsIsCitedAndForbiddenClaimsAreAbsent() + { + var record = EvaluatorTestData.Record( + finalResponse: "Evidence ev-metric-latency explains the spike.", + telemetry: [EvaluatorTestData.Metric("ev-metric-latency")], + requiredEvidenceIds: ["ev-metric-latency"], + forbiddenClaims: ["database saturation"]); + + AnalysisResult result = TelemetryEvidenceEvaluator.Analyze(record); + + Assert.True(result.Passed, result.Reason); + } + + [Fact] + public void Analyze_FailsMissingTelemetryMissingCitationAndForbiddenClaim() + { + var record = EvaluatorTestData.Record( + finalResponse: "The issue is database saturation.", + telemetry: [EvaluatorTestData.Metric("ev-present")], + requiredEvidenceIds: ["ev-missing", "ev-present"], + forbiddenClaims: ["database saturation"]); + + AnalysisResult result = TelemetryEvidenceEvaluator.Analyze(record); + + Assert.False(result.Passed); + Assert.Contains("missing telemetry: ev-missing", result.Reason, StringComparison.Ordinal); + Assert.Contains("missing citations: ev-missing, ev-present", result.Reason, StringComparison.Ordinal); + Assert.Contains("forbidden claims: database saturation", result.Reason, StringComparison.Ordinal); + } + + [Fact] + public async Task EvaluateAsync_ReturnsPassedMetricForGroundedAnswer() + { + var record = EvaluatorTestData.Record( + finalResponse: "Evidence ev-log-error supports the conclusion.", + telemetry: [EvaluatorTestData.Metric("ev-log-error")], + requiredEvidenceIds: ["ev-log-error"]); + + EvaluationResult result = await new TelemetryEvidenceEvaluator(record).EvaluateAsync( + [], + new ChatResponse(new ChatMessage(ChatRole.Assistant, record.FinalResponse)), + cancellationToken: TestContext.Current.CancellationToken); + + BooleanMetric metric = result.Get("qyl.telemetry.evidence"); + Assert.True(metric.Value); + Assert.False(metric.Interpretation?.Failed); + } +} diff --git a/evals/Qyl.Observability.Evaluation.Tests/ToolCallAccuracyEvaluatorTests.cs b/evals/Qyl.Observability.Evaluation.Tests/ToolCallAccuracyEvaluatorTests.cs new file mode 100644 index 000000000..0f65787fa --- /dev/null +++ b/evals/Qyl.Observability.Evaluation.Tests/ToolCallAccuracyEvaluatorTests.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Evaluation; +using Qyl.Observability.Evaluation.Evaluators; + +namespace Qyl.Observability.Evaluation.Tests; + +public sealed class ToolCallAccuracyEvaluatorTests +{ + [Fact] + public void Analyze_MatchesNestedJsonSemantically() + { + var record = EvaluatorTestData.Record( + toolCalls: + [ + EvaluatorTestData.ToolCall("qyl.query.traces", """{"filter":{"duration_ms":1000,"service":"checkout-api"},"window":"15m"}""") + ], + expectedToolCalls: + [ + EvaluatorTestData.ExpectedToolCall("QYL.QUERY.TRACES", """{"filter":{"service":"checkout-api","duration_ms":1000}}""") + ]); + + AnalysisResult result = ToolCallAccuracyEvaluator.Analyze(record); + + Assert.True(result.Passed, result.Reason); + } + + [Fact] + public void Analyze_FailsWhenJsonValueIsNotSemanticallyEqual() + { + var record = EvaluatorTestData.Record( + toolCalls: [EvaluatorTestData.ToolCall("qyl.query.traces", """{"window":"1h"}""")], + expectedToolCalls: [EvaluatorTestData.ExpectedToolCall("qyl.query.traces", """{"window":"15m"}""")]); + + AnalysisResult result = ToolCallAccuracyEvaluator.Analyze(record); + + Assert.False(result.Passed); + Assert.Contains("qyl.query.traces", result.Reason, StringComparison.Ordinal); + } + + [Fact] + public void Analyze_ConsumesEachActualToolCallOnce() + { + var record = EvaluatorTestData.Record( + toolCalls: [EvaluatorTestData.ToolCall("qyl.query.logs", """{"service":"payments-api"}""")], + expectedToolCalls: + [ + EvaluatorTestData.ExpectedToolCall("qyl.query.logs", """{"service":"payments-api"}"""), + EvaluatorTestData.ExpectedToolCall("qyl.query.logs", """{"service":"payments-api"}""") + ]); + + AnalysisResult result = ToolCallAccuracyEvaluator.Analyze(record); + + Assert.False(result.Passed); + } + + [Fact] + public async Task EvaluateAsync_ReturnsFailedMetricForMismatchedToolCall() + { + var record = EvaluatorTestData.Record( + toolCalls: [EvaluatorTestData.ToolCall("qyl.query.metrics", """{"window":"1h"}""")], + expectedToolCalls: [EvaluatorTestData.ExpectedToolCall("qyl.query.metrics", """{"window":"15m"}""")]); + + EvaluationResult result = await new ToolCallAccuracyEvaluator(record).EvaluateAsync( + [], + new ChatResponse(new ChatMessage(ChatRole.Assistant, "")), + cancellationToken: TestContext.Current.CancellationToken); + + BooleanMetric metric = result.Get("qyl.tool.call.accuracy"); + Assert.False(metric.Value); + Assert.True(metric.Interpretation?.Failed); + } +} diff --git a/evals/Qyl.Observability.Evaluation.Tests/TraceCorrelationEvaluatorTests.cs b/evals/Qyl.Observability.Evaluation.Tests/TraceCorrelationEvaluatorTests.cs new file mode 100644 index 000000000..70c24bdf3 --- /dev/null +++ b/evals/Qyl.Observability.Evaluation.Tests/TraceCorrelationEvaluatorTests.cs @@ -0,0 +1,96 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Evaluation; +using Qyl.Observability.Evaluation.Evaluators; + +namespace Qyl.Observability.Evaluation.Tests; + +public sealed class TraceCorrelationEvaluatorTests +{ + [Fact] + public void Analyze_PassesWhenParentResolvesInsideSameTrace() + { + var record = EvaluatorTestData.Record( + telemetry: + [ + EvaluatorTestData.Span("root", "trace-1", "span-root"), + EvaluatorTestData.Span("child", "trace-1", "span-child", "span-root") + ]); + + AnalysisResult result = TraceCorrelationEvaluator.Analyze(record); + + Assert.True(result.Passed, result.Reason); + } + + [Fact] + public void Analyze_FailsMissingTraceIdEvenWhenSpanIdsWouldCollide() + { + var record = EvaluatorTestData.Record( + telemetry: + [ + EvaluatorTestData.Span("root", null, "span-root"), + EvaluatorTestData.Span("child", null, "span-child", "span-root") + ]); + + AnalysisResult result = TraceCorrelationEvaluator.Analyze(record); + + Assert.False(result.Passed); + Assert.Contains("root:traceId", result.Reason, StringComparison.Ordinal); + Assert.Contains("child:traceId", result.Reason, StringComparison.Ordinal); + } + + [Fact] + public void Analyze_FailsMissingSpanId() + { + var record = EvaluatorTestData.Record( + telemetry: [EvaluatorTestData.Span("broken", "trace-1", null)]); + + AnalysisResult result = TraceCorrelationEvaluator.Analyze(record); + + Assert.False(result.Passed); + Assert.Contains("broken:spanId", result.Reason, StringComparison.Ordinal); + } + + [Fact] + public void Analyze_FailsDuplicateTraceSpanKey() + { + var record = EvaluatorTestData.Record( + telemetry: + [ + EvaluatorTestData.Span("first", "trace-1", "span-1"), + EvaluatorTestData.Span("second", "trace-1", "span-1") + ]); + + AnalysisResult result = TraceCorrelationEvaluator.Analyze(record); + + Assert.False(result.Passed); + Assert.Contains("duplicate span keys: trace-1/span-1", result.Reason, StringComparison.Ordinal); + } + + [Fact] + public void Analyze_FailsMissingParent() + { + var record = EvaluatorTestData.Record( + telemetry: [EvaluatorTestData.Span("child", "trace-1", "span-child", "span-missing")]); + + AnalysisResult result = TraceCorrelationEvaluator.Analyze(record); + + Assert.False(result.Passed); + Assert.Contains("child:span-missing", result.Reason, StringComparison.Ordinal); + } + + [Fact] + public async Task EvaluateAsync_ReturnsFailedMetricForBrokenTrace() + { + var record = EvaluatorTestData.Record( + telemetry: [EvaluatorTestData.Span("child", "trace-1", "span-child", "span-missing")]); + + EvaluationResult result = await new TraceCorrelationEvaluator(record).EvaluateAsync( + [], + new ChatResponse(new ChatMessage(ChatRole.Assistant, "")), + cancellationToken: TestContext.Current.CancellationToken); + + BooleanMetric metric = result.Get("qyl.trace.correlation"); + Assert.False(metric.Value); + Assert.True(metric.Interpretation?.Failed); + } +} diff --git a/evals/Qyl.Observability.Evaluation/Evaluators/ToolCallAccuracyEvaluator.cs b/evals/Qyl.Observability.Evaluation/Evaluators/ToolCallAccuracyEvaluator.cs index 3b57b3861..d1966d244 100644 --- a/evals/Qyl.Observability.Evaluation/Evaluators/ToolCallAccuracyEvaluator.cs +++ b/evals/Qyl.Observability.Evaluation/Evaluators/ToolCallAccuracyEvaluator.cs @@ -88,5 +88,5 @@ private static bool ArgumentsContainExpectedSubset( } private static bool JsonValuesEqual(JsonElement actual, JsonElement expected) - => actual.ValueKind == expected.ValueKind && actual.ToString().Equals(expected.ToString(), StringComparison.Ordinal); + => JsonElement.DeepEquals(actual, expected); } diff --git a/evals/Qyl.Observability.Evaluation/Evaluators/TraceCorrelationEvaluator.cs b/evals/Qyl.Observability.Evaluation/Evaluators/TraceCorrelationEvaluator.cs index 65b9ae598..23d147b35 100644 --- a/evals/Qyl.Observability.Evaluation/Evaluators/TraceCorrelationEvaluator.cs +++ b/evals/Qyl.Observability.Evaluation/Evaluators/TraceCorrelationEvaluator.cs @@ -22,32 +22,77 @@ public ValueTask EvaluateAsync( public static AnalysisResult Analyze(ObservabilityEvaluationRecord record) { - List spans = [.. record.Telemetry.Where(static telemetry => IsSpan(telemetry) && telemetry.SpanId is not null)]; - HashSet spanKeys = [.. spans.Select(static span => BuildSpanKey(span.TraceId, span.SpanId))]; - List missingParents = []; + List spans = [.. record.Telemetry.Where(static telemetry => IsSpan(telemetry))]; + List correlatableSpans = []; + List invalidSpans = []; foreach (TelemetryEvidenceRecord span in spans) { - if (span.ParentSpanId is null) + if (string.IsNullOrWhiteSpace(span.TraceId)) + { + invalidSpans.Add($"{span.Id}:traceId"); + } + + if (string.IsNullOrWhiteSpace(span.SpanId)) + { + invalidSpans.Add($"{span.Id}:spanId"); + } + + if (!string.IsNullOrWhiteSpace(span.TraceId) && !string.IsNullOrWhiteSpace(span.SpanId)) + { + correlatableSpans.Add(span); + } + } + + string[] duplicateSpanKeys = [.. correlatableSpans + .GroupBy(static span => BuildSpanKey(span.TraceId!, span.SpanId!)) + .Where(static group => group.Count() > 1) + .Select(static group => group.Key)]; + + HashSet spanKeys = [.. correlatableSpans.Select(static span => BuildSpanKey(span.TraceId!, span.SpanId!))]; + List missingParents = []; + + foreach (TelemetryEvidenceRecord span in correlatableSpans) + { + if (string.IsNullOrWhiteSpace(span.ParentSpanId)) { continue; } - string parentKey = BuildSpanKey(span.TraceId, span.ParentSpanId); + string parentKey = BuildSpanKey(span.TraceId!, span.ParentSpanId); if (!spanKeys.Contains(parentKey)) { missingParents.Add($"{span.Id}:{span.ParentSpanId}"); } } - return missingParents.Count == 0 - ? AnalysisResult.Pass("All span parent references resolve within their trace.") - : AnalysisResult.Fail($"missing parent spans: {string.Join(", ", missingParents)}"); + if (invalidSpans.Count == 0 && duplicateSpanKeys.Length == 0 && missingParents.Count == 0) + { + return AnalysisResult.Pass("All span identifiers are complete and parent references resolve within their trace."); + } + + List reasons = []; + if (invalidSpans.Count > 0) + { + reasons.Add($"invalid spans: {string.Join(", ", invalidSpans)}"); + } + + if (duplicateSpanKeys.Length > 0) + { + reasons.Add($"duplicate span keys: {string.Join(", ", duplicateSpanKeys)}"); + } + + if (missingParents.Count > 0) + { + reasons.Add($"missing parent spans: {string.Join(", ", missingParents)}"); + } + + return AnalysisResult.Fail(string.Join("; ", reasons)); } private static bool IsSpan(TelemetryEvidenceRecord telemetry) => telemetry.SignalType.Equals("span", StringComparison.OrdinalIgnoreCase); - private static string BuildSpanKey(string? traceId, string? spanId) - => string.Concat(traceId ?? string.Empty, "/", spanId ?? string.Empty); + private static string BuildSpanKey(string traceId, string spanId) + => string.Concat(traceId, "/", spanId); } diff --git a/evals/Qyl.Observability.Evaluation/Properties/AssemblyInfo.cs b/evals/Qyl.Observability.Evaluation/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..0c142cf28 --- /dev/null +++ b/evals/Qyl.Observability.Evaluation/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Qyl.Observability.Evaluation.Tests")] diff --git a/qyl.slnx b/qyl.slnx index fedc8a901..e893e42b8 100644 --- a/qyl.slnx +++ b/qyl.slnx @@ -36,6 +36,7 @@ +