From 83c9e6113a6d11f8ec1e70b4bba6bd37b28793f2 Mon Sep 17 00:00:00 2001 From: ancplua Date: Sun, 9 Aug 2026 02:02:37 +0200 Subject: [PATCH] Add agent diagnostic telemetry vocabulary --- .../SemconvRegistryFacts.g.cs | 18 ++- .../Attributes/Qyl/QylAttributes.g.cs | 122 +++++++++++++++ .../Names/QylTelemetryNames.g.cs | 11 +- .../Resources/qyl-registry.json | 141 +++++++++++++++++- .../scripts/emit_attributes.py | 6 +- .../AgentDiagnosticVocabularyTests.cs | 70 +++++++++ .../Qyl0200TelemetryNameAnalyzerTests.cs | 15 +- .../RegistryDerivedAnalyzerTests.cs | 42 ++++++ 8 files changed, 416 insertions(+), 9 deletions(-) create mode 100644 tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/AgentDiagnosticVocabularyTests.cs diff --git a/src/Qyl.Telemetry.SemanticConventions.Analyzers/SemconvRegistryFacts.g.cs b/src/Qyl.Telemetry.SemanticConventions.Analyzers/SemconvRegistryFacts.g.cs index 932cccf..f8afce0 100644 --- a/src/Qyl.Telemetry.SemanticConventions.Analyzers/SemconvRegistryFacts.g.cs +++ b/src/Qyl.Telemetry.SemanticConventions.Analyzers/SemconvRegistryFacts.g.cs @@ -793,6 +793,15 @@ internal static class SemconvRegistryFacts ["process.vpid"] = SemconvAttributeValueKind.Integer, ["process.working_directory"] = SemconvAttributeValueKind.String, ["profile.frame.type"] = SemconvAttributeValueKind.String, + ["qyl.agent.diagnostic.check.count"] = SemconvAttributeValueKind.Integer, + ["qyl.agent.diagnostic.check.failed_count"] = SemconvAttributeValueKind.Integer, + ["qyl.agent.diagnostic.extension.id"] = SemconvAttributeValueKind.String, + ["qyl.agent.diagnostic.format.version"] = SemconvAttributeValueKind.Integer, + ["qyl.agent.diagnostic.outcome"] = SemconvAttributeValueKind.String, + ["qyl.agent.diagnostic.phase"] = SemconvAttributeValueKind.String, + ["qyl.agent.diagnostic.probe.id"] = SemconvAttributeValueKind.String, + ["qyl.agent.diagnostic.snapshot.id"] = SemconvAttributeValueKind.String, + ["qyl.agent.diagnostic.variable.count"] = SemconvAttributeValueKind.Integer, ["qyl.capability.id"] = SemconvAttributeValueKind.String, ["qyl.capability.kind"] = SemconvAttributeValueKind.String, ["qyl.exception.source"] = SemconvAttributeValueKind.String, @@ -806,6 +815,11 @@ internal static class SemconvRegistryFacts ["qyl.mcp.test_case.id"] = SemconvAttributeValueKind.String, ["qyl.mcp.tool.name"] = SemconvAttributeValueKind.String, ["qyl.project.id"] = SemconvAttributeValueKind.String, + ["qyl.workflow.agent.id"] = SemconvAttributeValueKind.String, + ["qyl.workflow.attempt.id"] = SemconvAttributeValueKind.String, + ["qyl.workflow.event.id"] = SemconvAttributeValueKind.String, + ["qyl.workflow.run.id"] = SemconvAttributeValueKind.String, + ["qyl.workflow.tool_call.id"] = SemconvAttributeValueKind.String, ["qyl.workspace.id"] = SemconvAttributeValueKind.String, ["rpc.connect_rpc.error_code"] = SemconvAttributeValueKind.String, ["rpc.grpc.status_code"] = SemconvAttributeValueKind.String, @@ -1137,6 +1151,8 @@ private static readonly (string Prefix, SemconvAttributeValueKind Kind)[] Templa ["process.paging.fault_type"] = new[] { "major", "minor" }, ["process.state"] = new[] { "defunct", "running", "sleeping", "stopped" }, ["profile.frame.type"] = new[] { "beam", "cpython", "dotnet", "go", "jvm", "kernel", "luajit", "native", "perl", "php", "ruby", "rust", "v8js" }, + ["qyl.agent.diagnostic.outcome"] = new[] { "fail", "not_evaluated", "pass", "unknown" }, + ["qyl.agent.diagnostic.phase"] = new[] { "checkpoint", "error", "input", "output" }, ["rpc.connect_rpc.error_code"] = new[] { "aborted", "already_exists", "cancelled", "data_loss", "deadline_exceeded", "failed_precondition", "internal", "invalid_argument", "not_found", "out_of_range", "permission_denied", "resource_exhausted", "unauthenticated", "unavailable", "unimplemented", "unknown" }, ["rpc.grpc.status_code"] = new[] { "0", "1", "10", "11", "12", "13", "14", "15", "16", "2", "3", "4", "5", "6", "7", "8", "9" }, ["rpc.message.type"] = new[] { "RECEIVED", "SENT" }, @@ -1190,7 +1206,7 @@ private static readonly (string Prefix, SemconvAttributeValueKind Kind)[] Templa new(new[] { "Qyl.Collector", "Qyl.Instrumentation.ErrorCapture", "Qyl.OpenTelemetry.AutoInstrumentation", "Qyl.OpenTelemetry.AutoInstrumentation.Database", "Qyl.OpenTelemetry.AutoInstrumentation.NServiceBus", "System.Runtime" }, StringComparer.Ordinal); private static readonly HashSet KnownEventNames = - new(new[] { "app.crash", "app.jank", "app.screen.click", "app.widget.click", "az.resource.log", "azure.resource.log", "browser.web_vital", "db.client.operation.exception", "device.app.lifecycle", "exception", "faas.invocation.exception", "feature_flag.evaluation", "gen_ai.client.inference.operation.details", "gen_ai.client.operation.exception", "gen_ai.evaluation.result", "http.client.request.exception", "http.server.request.exception", "messaging.create.exception", "messaging.process.exception", "messaging.receive.exception", "messaging.send.exception", "messaging.settle.exception", "qyl.http.client", "qyl.rpc.grpc", "rpc.client.call.exception", "rpc.message", "rpc.server.call.exception", "session.end", "session.start" }, StringComparer.Ordinal); + new(new[] { "app.crash", "app.jank", "app.screen.click", "app.widget.click", "az.resource.log", "azure.resource.log", "browser.web_vital", "db.client.operation.exception", "device.app.lifecycle", "exception", "faas.invocation.exception", "feature_flag.evaluation", "gen_ai.client.inference.operation.details", "gen_ai.client.operation.exception", "gen_ai.evaluation.result", "http.client.request.exception", "http.server.request.exception", "messaging.create.exception", "messaging.process.exception", "messaging.receive.exception", "messaging.send.exception", "messaging.settle.exception", "qyl.agent.diagnostic.snapshot", "qyl.http.client", "qyl.rpc.grpc", "rpc.client.call.exception", "rpc.message", "rpc.server.call.exception", "session.end", "session.start" }, StringComparer.Ordinal); internal static readonly SemconvSpanRule[] SpanRules = { diff --git a/src/Qyl.Telemetry.SemanticConventions.Incubating/Attributes/Qyl/QylAttributes.g.cs b/src/Qyl.Telemetry.SemanticConventions.Incubating/Attributes/Qyl/QylAttributes.g.cs index 59ca8f7..f551c76 100644 --- a/src/Qyl.Telemetry.SemanticConventions.Incubating/Attributes/Qyl/QylAttributes.g.cs +++ b/src/Qyl.Telemetry.SemanticConventions.Incubating/Attributes/Qyl/QylAttributes.g.cs @@ -13,6 +13,103 @@ namespace Qyl.Telemetry.SemanticConventions.Incubating.Attributes.Qyl; /// Qyl Attributes (incubating). public static class QylAttributes { + /// + /// Number of machine-readable checks in an agent diagnostic state frame. + /// + public const string AgentDiagnosticCheckCount = "qyl.agent.diagnostic.check.count"; + + /// + /// Number of failed checks in an agent diagnostic state frame. + /// + public const string AgentDiagnosticCheckFailedCount = "qyl.agent.diagnostic.check.failed_count"; + + /// + /// Stable discriminator of the machine-readable agent diagnostic extension. + /// + public const string AgentDiagnosticExtensionId = "qyl.agent.diagnostic.extension.id"; + + /// + /// Version of the machine-readable agent diagnostic state-frame format. + /// + public const string AgentDiagnosticFormatVersion = "qyl.agent.diagnostic.format.version"; + + /// + /// Deterministically computed aggregate outcome of an agent diagnostic state frame. + /// + public const string AgentDiagnosticOutcome = "qyl.agent.diagnostic.outcome"; + + /// + /// Values for the AgentDiagnosticOutcome attribute. + /// + public static class AgentDiagnosticOutcomeValues + { + /// + /// At least one evaluated diagnostic check failed. + /// + public const string Fail = "fail"; + + /// + /// The state frame contained no diagnostic checks. + /// + public const string NotEvaluated = "not_evaluated"; + + /// + /// Every evaluated diagnostic check passed. + /// + public const string Pass = "pass"; + + /// + /// No failed check was observed, but at least one check could not be evaluated. + /// + public const string Unknown = "unknown"; + } + + /// + /// Program boundary at which an agent diagnostic state frame was captured. + /// + public const string AgentDiagnosticPhase = "qyl.agent.diagnostic.phase"; + + /// + /// Values for the AgentDiagnosticPhase attribute. + /// + public static class AgentDiagnosticPhaseValues + { + /// + /// State captured at an explicit verification checkpoint. + /// + public const string Checkpoint = "checkpoint"; + + /// + /// State captured on an error path. + /// + public const string Error = "error"; + + /// + /// State captured before the probed operation. + /// + public const string Input = "input"; + + /// + /// State captured after the probed operation. + /// + public const string Output = "output"; + } + + /// + /// Bounded machine identifier of the diagnostic probe that captured the state frame. + /// + public const string AgentDiagnosticProbeId = "qyl.agent.diagnostic.probe.id"; + + /// + /// Stable identifier of one immutable agent diagnostic state frame. + /// + public const string AgentDiagnosticSnapshotId = "qyl.agent.diagnostic.snapshot.id"; + + /// + /// Number of dynamic variables described by an agent diagnostic state frame. + /// + public const string AgentDiagnosticVariableCount = "qyl.agent.diagnostic.variable.count"; + /// /// Stable identifier of the qyl capability that produced the telemetry. /// @@ -78,6 +175,31 @@ public static class QylAttributes /// public const string ProjectId = "qyl.project.id"; + /// + /// Stable workflow agent identifier mirrored into telemetry for journal correlation. + /// + public const string WorkflowAgentId = "qyl.workflow.agent.id"; + + /// + /// Stable workflow attempt identifier mirrored into telemetry for journal correlation. + /// + public const string WorkflowAttemptId = "qyl.workflow.attempt.id"; + + /// + /// Stable immutable workflow journal event identifier mirrored into telemetry for correlation. + /// + public const string WorkflowEventId = "qyl.workflow.event.id"; + + /// + /// Stable workflow run identifier mirrored into telemetry for journal correlation. + /// + public const string WorkflowRunId = "qyl.workflow.run.id"; + + /// + /// Stable workflow tool-call identifier mirrored into telemetry for journal correlation. + /// + public const string WorkflowToolCallId = "qyl.workflow.tool_call.id"; + /// /// qyl workspace identifier the telemetry belongs to. /// diff --git a/src/Qyl.Telemetry.SemanticConventions.Incubating/Names/QylTelemetryNames.g.cs b/src/Qyl.Telemetry.SemanticConventions.Incubating/Names/QylTelemetryNames.g.cs index 66caf75..8685d70 100644 --- a/src/Qyl.Telemetry.SemanticConventions.Incubating/Names/QylTelemetryNames.g.cs +++ b/src/Qyl.Telemetry.SemanticConventions.Incubating/Names/QylTelemetryNames.g.cs @@ -55,17 +55,22 @@ public static class Scopes } /// - /// Names of the qyl-owned DiagnosticSource bridge events. + /// Names of qyl-owned Activity and DiagnosticSource events. /// public static class Events { /// - /// The qyl.http.client bridge event. + /// The qyl.agent.diagnostic.snapshot telemetry event. + /// + public const string QylAgentDiagnosticSnapshot = "qyl.agent.diagnostic.snapshot"; + + /// + /// The qyl.http.client telemetry event. /// public const string QylHttpClient = "qyl.http.client"; /// - /// The qyl.rpc.grpc bridge event. + /// The qyl.rpc.grpc telemetry event. /// public const string QylRpcGrpc = "qyl.rpc.grpc"; } diff --git a/src/Qyl.Telemetry.SemanticConventions.SourceGeneration/Resources/qyl-registry.json b/src/Qyl.Telemetry.SemanticConventions.SourceGeneration/Resources/qyl-registry.json index a7f834f..f7a93be 100644 --- a/src/Qyl.Telemetry.SemanticConventions.SourceGeneration/Resources/qyl-registry.json +++ b/src/Qyl.Telemetry.SemanticConventions.SourceGeneration/Resources/qyl-registry.json @@ -1,6 +1,12 @@ { - "description": "qyl-owned vocabulary, first-class next to the pinned upstream registries (qyl architecture, loop 1). Attributes here are qyl.* names the platform emits and the collector must recognize; scope_names are the ActivitySource/Meter names qyl constructs; event_names are qyl-owned DiagnosticSource bridge event names. Consumed by emit_analyzer_registry.py (QYL0200/QYL0201 allowlists). Entries under qyl.mcp.* are the experimental staging namespace: deletion-targeted on every upstream registry bump that lands an equivalent.", + "description": "qyl-owned vocabulary, first-class next to the pinned upstream registries (qyl architecture, loop 1). Attributes here are qyl.* names the platform emits and the collector must recognize; scope_names are the ActivitySource/Meter names qyl constructs; event_names are qyl-owned Activity or DiagnosticSource event names. Consumed by emit_analyzer_registry.py (QYL0200/QYL0201 allowlists). Entries under qyl.mcp.* are the experimental staging namespace: deletion-targeted on every upstream registry bump that lands an equivalent.", "attributes": [ + { + "key": "qyl.agent.diagnostic.extension.id", + "type": "string", + "stability": "development", + "brief": "Stable discriminator of the machine-readable agent diagnostic extension." + }, { "key": "qyl.capability.id", "type": "string", @@ -25,6 +31,138 @@ "stability": "development", "brief": "qyl auto-instrumentation domain that emitted the span or metric." }, + { + "key": "qyl.agent.diagnostic.snapshot.id", + "type": "string", + "stability": "development", + "brief": "Stable identifier of one immutable agent diagnostic state frame." + }, + { + "key": "qyl.agent.diagnostic.format.version", + "type": "int", + "stability": "development", + "brief": "Version of the machine-readable agent diagnostic state-frame format." + }, + { + "key": "qyl.agent.diagnostic.probe.id", + "type": "string", + "stability": "development", + "brief": "Bounded machine identifier of the diagnostic probe that captured the state frame." + }, + { + "key": "qyl.agent.diagnostic.phase", + "type": { + "members": [ + { + "id": "input", + "value": "input", + "stability": "development", + "brief": "State captured before the probed operation." + }, + { + "id": "output", + "value": "output", + "stability": "development", + "brief": "State captured after the probed operation." + }, + { + "id": "error", + "value": "error", + "stability": "development", + "brief": "State captured on an error path." + }, + { + "id": "checkpoint", + "value": "checkpoint", + "stability": "development", + "brief": "State captured at an explicit verification checkpoint." + } + ] + }, + "stability": "development", + "brief": "Program boundary at which an agent diagnostic state frame was captured." + }, + { + "key": "qyl.agent.diagnostic.outcome", + "type": { + "members": [ + { + "id": "pass", + "value": "pass", + "stability": "development", + "brief": "Every evaluated diagnostic check passed." + }, + { + "id": "fail", + "value": "fail", + "stability": "development", + "brief": "At least one evaluated diagnostic check failed." + }, + { + "id": "unknown", + "value": "unknown", + "stability": "development", + "brief": "No failed check was observed, but at least one check could not be evaluated." + }, + { + "id": "not_evaluated", + "value": "not_evaluated", + "stability": "development", + "brief": "The state frame contained no diagnostic checks." + } + ] + }, + "stability": "development", + "brief": "Deterministically computed aggregate outcome of an agent diagnostic state frame." + }, + { + "key": "qyl.agent.diagnostic.variable.count", + "type": "int", + "stability": "development", + "brief": "Number of dynamic variables described by an agent diagnostic state frame." + }, + { + "key": "qyl.agent.diagnostic.check.count", + "type": "int", + "stability": "development", + "brief": "Number of machine-readable checks in an agent diagnostic state frame." + }, + { + "key": "qyl.agent.diagnostic.check.failed_count", + "type": "int", + "stability": "development", + "brief": "Number of failed checks in an agent diagnostic state frame." + }, + { + "key": "qyl.workflow.run.id", + "type": "string", + "stability": "development", + "brief": "Stable workflow run identifier mirrored into telemetry for journal correlation." + }, + { + "key": "qyl.workflow.event.id", + "type": "string", + "stability": "development", + "brief": "Stable immutable workflow journal event identifier mirrored into telemetry for correlation." + }, + { + "key": "qyl.workflow.attempt.id", + "type": "string", + "stability": "development", + "brief": "Stable workflow attempt identifier mirrored into telemetry for journal correlation." + }, + { + "key": "qyl.workflow.agent.id", + "type": "string", + "stability": "development", + "brief": "Stable workflow agent identifier mirrored into telemetry for journal correlation." + }, + { + "key": "qyl.workflow.tool_call.id", + "type": "string", + "stability": "development", + "brief": "Stable workflow tool-call identifier mirrored into telemetry for journal correlation." + }, { "key": "qyl.project.id", "type": "string", @@ -96,6 +234,7 @@ "System.Runtime" ], "event_names": [ + "qyl.agent.diagnostic.snapshot", "qyl.http.client", "qyl.rpc.grpc" ] diff --git a/src/Qyl.Telemetry.SemanticConventions.SourceGeneration/scripts/emit_attributes.py b/src/Qyl.Telemetry.SemanticConventions.SourceGeneration/scripts/emit_attributes.py index 2969ba4..02b7c90 100644 --- a/src/Qyl.Telemetry.SemanticConventions.SourceGeneration/scripts/emit_attributes.py +++ b/src/Qyl.Telemetry.SemanticConventions.SourceGeneration/scripts/emit_attributes.py @@ -637,7 +637,7 @@ def attr_ident(a): # --------------------------------------------------------------------------- # -# qyl-owned scope + bridge event names # +# qyl-owned scope + telemetry event names # # --------------------------------------------------------------------------- # NAMES_NAMESPACE = "Qyl.Telemetry.SemanticConventions.Incubating.Names" NAMES_CLASS = "QylTelemetryNames" @@ -700,7 +700,7 @@ def emit_names_file(qyl_registry) -> str: w.line(f"{pad}}}") w.line() - w.summary(pad, ["Names of the qyl-owned DiagnosticSource bridge events."]) + w.summary(pad, ["Names of qyl-owned Activity and DiagnosticSource events."]) w.line(f"{pad}public static class Events") w.line(f"{pad}{{") first = True @@ -708,7 +708,7 @@ def emit_names_file(qyl_registry) -> str: if not first: w.line() first = False - w.summary(ipad, [f"The {escape_attribute(value)} bridge event."]) + w.summary(ipad, [f"The {escape_attribute(value)} telemetry event."]) w.line(f"{ipad}public const string {ident} = {csharp_string(value)};") w.line(f"{pad}}}") diff --git a/tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/AgentDiagnosticVocabularyTests.cs b/tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/AgentDiagnosticVocabularyTests.cs new file mode 100644 index 0000000..9e9f33b --- /dev/null +++ b/tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/AgentDiagnosticVocabularyTests.cs @@ -0,0 +1,70 @@ +using AwesomeAssertions; +using Qyl.Telemetry.SemanticConventions.Incubating.Attributes.Qyl; +using Qyl.Telemetry.SemanticConventions.Incubating.Names; +using Xunit; + +namespace Qyl.Telemetry.SemanticConventions.Pipeline.Tests; + +public sealed class AgentDiagnosticVocabularyTests +{ + [Fact] + public void Agent_diagnostic_snapshot_uses_one_fixed_event_and_fixed_summary_keys() + { + QylTelemetryNames.Events.QylAgentDiagnosticSnapshot.Should() + .Be("qyl.agent.diagnostic.snapshot"); + + (string Actual, string Expected)[] summaryKeys = + [ + (QylAttributes.AgentDiagnosticExtensionId, "qyl.agent.diagnostic.extension.id"), + (QylAttributes.AgentDiagnosticFormatVersion, "qyl.agent.diagnostic.format.version"), + (QylAttributes.AgentDiagnosticSnapshotId, "qyl.agent.diagnostic.snapshot.id"), + (QylAttributes.AgentDiagnosticProbeId, "qyl.agent.diagnostic.probe.id"), + (QylAttributes.AgentDiagnosticPhase, "qyl.agent.diagnostic.phase"), + (QylAttributes.AgentDiagnosticOutcome, "qyl.agent.diagnostic.outcome"), + (QylAttributes.AgentDiagnosticVariableCount, "qyl.agent.diagnostic.variable.count"), + (QylAttributes.AgentDiagnosticCheckCount, "qyl.agent.diagnostic.check.count"), + (QylAttributes.AgentDiagnosticCheckFailedCount, "qyl.agent.diagnostic.check.failed_count"), + ]; + + summaryKeys.Select(static item => item.Actual).Should().OnlyHaveUniqueItems(); + summaryKeys.Should().AllSatisfy(static item => item.Actual.Should().Be(item.Expected)); + } + + [Fact] + public void Agent_diagnostic_phase_and_outcome_values_are_machine_tokens() + { + string[] phases = + [ + QylAttributes.AgentDiagnosticPhaseValues.Input, + QylAttributes.AgentDiagnosticPhaseValues.Output, + QylAttributes.AgentDiagnosticPhaseValues.Error, + QylAttributes.AgentDiagnosticPhaseValues.Checkpoint, + ]; + string[] outcomes = + [ + QylAttributes.AgentDiagnosticOutcomeValues.Pass, + QylAttributes.AgentDiagnosticOutcomeValues.Fail, + QylAttributes.AgentDiagnosticOutcomeValues.Unknown, + QylAttributes.AgentDiagnosticOutcomeValues.NotEvaluated, + ]; + + phases.Should().Equal("input", "output", "error", "checkpoint"); + outcomes.Should().Equal("pass", "fail", "unknown", "not_evaluated"); + } + + [Fact] + public void Workflow_correlation_uses_fixed_qyl_keys() + { + (string Actual, string Expected)[] correlationKeys = + [ + (QylAttributes.WorkflowRunId, "qyl.workflow.run.id"), + (QylAttributes.WorkflowEventId, "qyl.workflow.event.id"), + (QylAttributes.WorkflowAttemptId, "qyl.workflow.attempt.id"), + (QylAttributes.WorkflowAgentId, "qyl.workflow.agent.id"), + (QylAttributes.WorkflowToolCallId, "qyl.workflow.tool_call.id"), + ]; + + correlationKeys.Select(static item => item.Actual).Should().OnlyHaveUniqueItems(); + correlationKeys.Should().AllSatisfy(static item => item.Actual.Should().Be(item.Expected)); + } +} diff --git a/tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/Qyl0200TelemetryNameAnalyzerTests.cs b/tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/Qyl0200TelemetryNameAnalyzerTests.cs index 4ff8186..74d46ba 100644 --- a/tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/Qyl0200TelemetryNameAnalyzerTests.cs +++ b/tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/Qyl0200TelemetryNameAnalyzerTests.cs @@ -36,6 +36,15 @@ public async Task Qyl0200_accepts_registry_attribute_keys_including_qyl_owned_en """ activity?.SetTag("http.request.method", "GET"); activity?.SetTag("qyl.instrumentation.domain", "http"); + activity?.SetTag("qyl.agent.diagnostic.extension.id", "qyl.agent.diagnostic.snapshot"); + activity?.SetTag("qyl.agent.diagnostic.format.version", 1); + activity?.SetTag("qyl.agent.diagnostic.snapshot.id", "snapshot-1"); + activity?.SetTag("qyl.agent.diagnostic.probe.id", "vcs.root.resolve"); + activity?.SetTag("qyl.agent.diagnostic.phase", "checkpoint"); + activity?.SetTag("qyl.agent.diagnostic.outcome", "pass"); + activity?.SetTag("qyl.agent.diagnostic.variable.count", 3); + activity?.SetTag("qyl.agent.diagnostic.check.count", 2); + activity?.SetTag("qyl.agent.diagnostic.check.failed_count", 0); """)); diagnostics.Should().BeEmpty(); @@ -119,7 +128,11 @@ public async Task Qyl0200_checks_activity_event_names() { var clean = await AnalyzerHarness.RunAsync( new Qyl0200TelemetryNameAnalyzer(), - ActivitySink("activity?.AddEvent(new ActivityEvent(\"exception\"));")); + ActivitySink( + """ + activity?.AddEvent(new ActivityEvent("exception")); + activity?.AddEvent(new ActivityEvent("qyl.agent.diagnostic.snapshot")); + """)); clean.Should().BeEmpty(); var flagged = await AnalyzerHarness.RunAsync( diff --git a/tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/RegistryDerivedAnalyzerTests.cs b/tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/RegistryDerivedAnalyzerTests.cs index 2b49ca1..355d6ed 100644 --- a/tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/RegistryDerivedAnalyzerTests.cs +++ b/tests/Qyl.Telemetry.SemanticConventions.Pipeline.Tests/RegistryDerivedAnalyzerTests.cs @@ -37,6 +37,48 @@ public async Task Qyl0013_accepts_registry_array_and_schema_governed_any_types() diagnostics.Should().BeEmpty(); } + [Fact] + public async Task Qyl0013_enforces_agent_diagnostic_summary_types() + { + var diagnostics = await AnalyzerHarness.RunAsync( + new Qyl0013IncorrectAttributeTypeAnalyzer(), + SinkSource( + """ + SetTag("qyl.agent.diagnostic.extension.id", "qyl.agent.diagnostic.snapshot"); + SetTag("qyl.agent.diagnostic.format.version", "1"); + SetTag("qyl.agent.diagnostic.snapshot.id", "snapshot-1"); + SetTag("qyl.agent.diagnostic.probe.id", "probe-1"); + SetTag("qyl.agent.diagnostic.phase", "checkpoint"); + SetTag("qyl.agent.diagnostic.outcome", "pass"); + SetTag("qyl.agent.diagnostic.variable.count", 3); + SetTag("qyl.agent.diagnostic.check.count", 2); + SetTag("qyl.agent.diagnostic.check.failed_count", 0); + """)); + + diagnostics.Should().ContainSingle(); + diagnostics[0].Id.Should().Be("QYL0013"); + diagnostics[0].GetMessage(CultureInfo.InvariantCulture).Should().Contain("integer"); + } + + [Fact] + public async Task Qyl0012_enforces_agent_diagnostic_phase_and_outcome_tokens() + { + var diagnostics = await AnalyzerHarness.RunAsync( + new Qyl0012InvalidAttributeValueAnalyzer(), + SinkSource( + """ + SetTag("qyl.agent.diagnostic.phase", "CHECKPOINT"); + SetTag("qyl.agent.diagnostic.outcome", "PASS"); + """)); + + diagnostics.Should().HaveCount(2); + diagnostics.Select(static diagnostic => diagnostic.Id).Should().OnlyContain(static id => id == "QYL0012"); + diagnostics.Select(static diagnostic => diagnostic.GetMessage(CultureInfo.InvariantCulture)) + .Should().Contain(static message => message.Contains("checkpoint", StringComparison.Ordinal)); + diagnostics.Select(static diagnostic => diagnostic.GetMessage(CultureInfo.InvariantCulture)) + .Should().Contain(static message => message.Contains("pass", StringComparison.Ordinal)); + } + [Fact] public async Task Qyl0012_reports_only_noncanonical_spelling_of_known_enum_values() {